aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 14:28:40 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 14:28:40 +0200
commit7e2d078fe1870d256ae47781bee6ac4f454edf24 (patch)
tree05689049fd48f01ebbdb9995d5189cba15cee052 /packages/meshbay-hub/src/meshbay_hub/api
parent813d18424ec57963bb56e6f40824a2db0ccce50d (diff)
parente6f895c473a0b19e7b186889c1836d3945bc880b (diff)
downloadmeshbay-7e2d078fe1870d256ae47781bee6ac4f454edf24.tar.gz
Merge branch 'fix/large-download-paths'
Concurrent-transfer limits, with the queue, the pause and the flag day. A node now caps how many transfers it runs at once (8 downloads, 8 uploads, node-wide) and how many one member may run in one group (2 by default, operator-signed). Beyond that the node answers "queued" and the client waits its turn, visibly, in the transfers panel — and a slot that frees starts whatever is next, skipping past a member who is at their own cap rather than letting them stall everyone behind them. Browsing is never subject to a slot: not the poster grid, not the covers, not opening a photo to look at it. That is structural — a transfer is what the transfers widget shows — and the exemption is bounded rather than open, at two files in flight per session, because an exemption with no bound is a leaseless branch under another name. Transfers can be cancelled, and now paused and resumed. A paused one holds nothing: its slot goes back at once and resuming rejoins the queue at the tail. Uploads survive the connection that started them and resume where the node stopped, asked for inside the seal rather than on a clear message. What they leave behind when they are abandoned is reaped, which closes a disk leak that predates this work. MNP 3.0 makes the lease compulsory and refuses 2.x at the handshake, with the desktop client checking `client.minimum` before connecting so an un-updated one says "update" instead of failing every connection in a protocol vocabulary. Fourteen defects were found on the way, eight of them by a person clicking Download and pasting a console — none of which 2075 tests could reach. Section 12 of ~/next/improve-downloads.md is that report, including the three this work introduced itself and the one that turned out to be caused by an instruction to hard-reload after each deployment. Node suite 1209 passed, hub suite 866 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/hub.py17
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/webapp.py26
2 files changed, 39 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
index 8223400..a48313d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
@@ -54,8 +54,21 @@ async def hub_pubkey():
# user "update to keep using this" before it becomes "this stopped working".
# Raise `minimum` only for a change a client genuinely cannot survive, and
# remember store review latency makes that expensive on Android.
-MIN_CLIENT_VERSION = "0.1.0"
-RECOMMENDED_CLIENT_VERSION = "0.1.0"
+# Raised on the MNP 3.0 flag day (2026-09-09). A client older than this speaks
+# MNP 2.x, cannot ask for a transfer lease, and is refused at the node's
+# handshake with `version_too_old` — a refusal in a protocol vocabulary that
+# surfaces as "the node will not talk to me". The client checks this field
+# before connecting and says something a person can act on instead.
+#
+# **This first raise does not reach the clients already installed**, and that is
+# understood rather than overlooked. `package.json` had drifted to "1.0.0" while
+# every other package was on 0.12.0, so an installed client announces a version
+# that sorts *above* this minimum and sails through the gate — then meets the
+# handshake refusal anyway. The operator is updating every client, node and hub
+# by hand for this flag day, which is what makes that acceptable exactly once.
+# The gate is in place for the next one, where it will work as intended.
+MIN_CLIENT_VERSION = "0.13.0"
+RECOMMENDED_CLIENT_VERSION = "0.13.0"
@router.get("/version")
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
index 3cfb208..96b93bb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
@@ -111,8 +111,30 @@ CSP = "; ".join([
"font-src 'self'",
"connect-src 'self' https: wss:",
"worker-src 'self'",
- f"frame-src {_RECAPTCHA_SRC}",
- "frame-ancestors 'none'",
+ # `'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
+ # never asked, and the page waits out its timeout for a download that cannot
+ # 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}",
+ # `'self'`, not `'none'`, and the difference is one same-origin iframe.
+ #
+ # The threat frame-ancestors answers is clickjacking: a *foreign* page
+ # framing this one and stealing clicks. `'self'` refuses every foreign
+ # origin exactly as `'none'` does — what it additionally allows is this
+ # origin framing itself, which is precisely how a streamed download works
+ # (a hidden iframe navigates to `/_mbdl/<id>` so the service worker is
+ # asked for the response it holds).
+ #
+ # Under `'none'` Firefox blocked that frame, the worker was never asked,
+ # and every large download waited out two 15-second timeouts and then fell
+ # through — on Firefox and Safari that is the only way to write a large
+ # file to disk. Chrome did not show it: its worker intercepts the
+ # navigation before the network response and its CSP are ever considered,
+ # which is why this looked like a Firefox-only problem for an afternoon.
+ "frame-ancestors 'self'",
"base-uri 'none'",
"form-action 'none'",
])