summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 14:00:22 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 14:00:22 +0200
commit53ea44cb03ef6f8d941f6c8c9446551b0c5cd1ac (patch)
tree0d38403f86a0b96d5076c6404b471df7d1d41aad /packages/meshbay-common/src
parentdee57df42a525cead93fa30b4e7fa38a489d5b11 (diff)
downloadmeshbay-53ea44cb03ef6f8d941f6c8c9446551b0c5cd1ac.tar.gz
feat: MNP 3.0 — a transfer needs a lease
Stage 4 of ~/next/improve-downloads.md, the flag day. Leases become compulsory and a 2.x peer is refused at the handshake. **The bound on leaseless reads (§3.4.1) did not exist, and it is what makes the rest mean anything.** Browsing a group is never subject to a transfer slot — that is an operator decision and a requirement: a member must be able to browse a group at capacity exactly as they browse an idle one. But "not leased" cannot mean "unbounded", or a client that simply omits `tr` transfers outside every cap and the caps are decoration. A session may now read two distinct files at once without a lease: one because a viewer looks at one file, two so that prefetching the next photo stays possible. A count of files and not a byte budget, because a RAW photo is 60-80 MB and is browsing while a 40 MB archive is a download, and no size threshold separates them. Thumbnails, posters and cover art never reach this check at all — they resolve out of the node's own cache. It is a fairness control among cooperating clients, in the company of `max_concurrent_streams`, and is not a defence against a member determined to saturate a node's disk. That member is a member, and the answer to them is `member revoke`. **MNP_VERSION and MNP_MIN_SUPPORTED both move to 3.0**, on both sides. The messages are additive; the requirement is not. An opt-in switch would leave a leaseless branch reachable on every node, which is finding C6's lesson — a transport that accepted a bare JWT — one feature later. **The desktop client now checks before it connects.** The SPA is served by the hub and picks up a new client on reload; the application ships its own interface, so an un-updated one would sign in, list groups, and fail every connection with `version_too_old` — a refusal in a protocol vocabulary with nothing anyone can act on. It asks `/v1/hub/version` for `client.minimum` and says so plainly instead. An unreachable hub is deliberately *not* "too old": a captive portal or a closed laptop must not make starting the application impossible. **Every package is aligned on 0.13.0.** `meshbay-client/package.json` had drifted to 1.0.0 while the Python packages were on 0.12.0 — invisible until something compared those numbers, and then load-bearing: an installed client announcing 1.0.0 sorts above a 0.13.0 minimum and walks through the gate meant to stop it. That is stated in the code rather than left to be rediscovered; it is acceptable exactly once, because the operator is updating every client, node and hub by hand for this flag day. A new test fails if two packages ever disagree again, and another fails if the hub would refuse the client the tree builds. Node suite 1209 passed, hub suite 861 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-common/src')
-rw-r--r--packages/meshbay-common/src/meshbay_common/__init__.py36
-rw-r--r--packages/meshbay-common/src/meshbay_common/handshake.py7
2 files changed, 40 insertions, 3 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py
index b64a6f2..1aaa269 100644
--- a/packages/meshbay-common/src/meshbay_common/__init__.py
+++ b/packages/meshbay-common/src/meshbay_common/__init__.py
@@ -1,6 +1,6 @@
"""MeshBay common — shared crypto primitives and protocol types."""
-__version__ = "0.12.0"
+__version__ = "0.13.0"
# 0.2: added PING/PONG, and `before`/`has_more` on chat history. Both are
# additive — an 0.1 peer sends no `before` and gets the newest page, which is
# what it wanted — so this is a MINOR bump, not a MAJOR one.
@@ -133,5 +133,37 @@ __version__ = "0.12.0"
# `index_progress` (counters only — see daemon.py `_push_index_progress`), the
# admin and configuration acks, and the media-metadata replies. The index at
# rest and file content on the operator's disk are unchanged.
-MNP_VERSION = "2.0"
+# **3.0 (2026-09-09): a transfer needs a lease, and a peer that cannot ask for
+# one is refused at the handshake.**
+#
+# `transfer_open` / `transfer_close` / `transfer_state` carry the lease a
+# download or an upload runs under; `file_req` gains an optional `tr` and
+# `file_upload` gains one beside the `upload_id` already in clear. All three are
+# in clear, like `index_progress` and for the same stated reason: `tr` is
+# opaque, `bytes` and `chunks` are numbers, and there is no filename and no path
+# anywhere in them. Putting one there to make a log line prettier is exactly the
+# trade `groupbox.py` exists to refuse.
+#
+# **The messages are additive; the requirement is not, and that is what makes
+# this MAJOR.** A 2.0 client sends no `tr`, so it is a leaseless reader — and a
+# leaseless reader is either refused as soon as it opens a third file, or it is
+# not refused and transfers outside every cap the operator set. An opt-in switch
+# ("enforce leases only for clients that speak 3.0") leaves that branch
+# reachable on every node, which is finding C6's lesson — a transport that
+# accepted a bare JWT — one feature later. It was already refused once, for chat
+# encryption, on 2026-09-07.
+#
+# Browsing is deliberately **not** leased and never will be: not the poster
+# grid, not the covers, not opening a photo to look at it. That exemption is
+# bounded rather than open (`transfers.LeaselessReads`, two files in flight per
+# session), because an exemption with no bound is the leaseless branch under
+# another name.
+#
+# **What it costs, stated plainly.** The SPA is served by the hub, so a browser
+# picks up the new client on reload. The desktop client ships its own UI, so an
+# un-updated one is locked out — which is why `GET /v1/hub/version` carries
+# `client.minimum` and the client checks it *before* connecting, and says "this
+# version can no longer connect" rather than showing a handshake refusal nobody
+# can act on.
+MNP_VERSION = "3.0"
MHP_VERSION = "0.1"
diff --git a/packages/meshbay-common/src/meshbay_common/handshake.py b/packages/meshbay-common/src/meshbay_common/handshake.py
index 188a8aa..65b4e85 100644
--- a/packages/meshbay-common/src/meshbay_common/handshake.py
+++ b/packages/meshbay-common/src/meshbay_common/handshake.py
@@ -78,7 +78,12 @@ HANDSHAKE_PREFIX = b"meshbay:mnp:handshake:v1"
# handshake and then discovering that every message it sends is rejected and
# every message it receives is unreadable. A stated refusal is a bug report; a
# chat that quietly does not work is a support case.
-MNP_MIN_SUPPORTED = "2.0"
+# 3.0 (2026-09-09): a transfer runs under a lease, and a 2.x peer cannot ask for
+# one. Admitting it would mean either refusing it later, per file, in a way it
+# has no vocabulary to understand — or serving it outside every cap the operator
+# set, which makes the caps decoration. Neither is honest, so it is refused
+# here, with a code and a sentence.
+MNP_MIN_SUPPORTED = "3.0"
ROLE_CLIENT = "client"
ROLE_NODE = "node"