| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
| |
sender_name and thread_id travel in clear beside the sealed envelope and were
stored and relayed whatever their type and size. A name longer than a username
or a thread id that is not a short id is now dropped (F-27).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Stream, transcode and subtitle failures sent the exception's text — operator
paths, versions — to the member. Fixed messages now, the cause in the log
(F-24).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
The desktop app writes the Mark-of-the-Web on each file it saves on Windows, as
a browser does. Bidirectional controls are reserved characters in a saved name
(portable-name.js and paths.sanitize_for_download, and again in the main
process), so a name cannot display one extension and carry another. The node
refuses uploads of files Windows Explorer acts on by itself: desktop.ini,
.lnk, .url, .scf, .library-ms, .searchConnector-ms (F-19).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
128 peer sessions on the node, at most 64 per account (the hub names the
account with each offer; the node's own account is not counted). One account
plays at most half the stream slots, rounded up, and runs two subtitle
extractions at once. Frames after the handshake are 8 MiB (was 64), decoded
with per-container bounds, and a frame refused for either ends the session
instead of jamming its buffer (F-16).
Sized for the heaviest real member: twenty groups on one node, three devices
and a tab, up to 52 sessions. Measured: ~0.15 MiB and one fd per idle session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
A name an upload in flight will take is reserved; each upload writes its own
`name.<tag>.part`; the finished file is published by a hard link, which
refuses an existing target, and takes the next free name if one appeared
meanwhile — the last ack names it. Two members sending one name at once wrote
one part and published it twice; a file copied in during an upload was
replaced (F-09).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
probe_video waits 30 s at most and kills ffprobe on a timeout or when its
caller gives up — a cancelled wait left the process running. The seek probe
kills what it timed out on. Stream, subtitle and enrichment requests no longer
hang on a file that keeps ffprobe busy (F-18, timeouts; the protocol
whitelist was dropped: ffmpeg already confines nested protocols of a local
input, measured on 8.0 against HLS and concat inputs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
The name is resolved off the event loop and every answer checked; the socket
is then opened to that IP literal through a pinned httpcore backend, TLS still
verifying the certificate for the name, and no proxy from the environment.
A name that answers clean and then with a LAN address no longer gets a
request sent there, and a slow name no longer stalls the node (F-12).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
The desktop main process builds every transcript itself from fields
(transcripts.js) and signs no raw bytes; the page's identity has the same
contract (crypto.js transcriptFor). The keyring seals no bundle while browser
access is off. On the node, device_add must redeem a pending request filed by
the same keys, and device_revoke is signed under its own prefix
(meshbay:device_revoke:v1), so a retirement signature admits nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The handshake opened a session for anyone holding the group key with a hub
token naming the group; the roster was consulted only when wrapping the key
in a join. A member revoked or unpinned on the node but still a member on
the hub kept a full session with the key they already held — and was handed
the chat epoch their removal had just opened, since chat keys go to any
session. An honest client never met this (it asks for the key through
join_request every time); one that kept the key did not have to.
- After the proof, the node asks the roster and refuses with
`not_authorized_for_group` unless the account is an active member of the
group or the node's operator.
- A removal from any door — MNP, the node page, the CLI — now opens a new
chat epoch in each group the person could read, broadcasts it, and closes
every connection they hold (`ops.members._after_removal`). The CLI and the
node page did neither.
- Design §5.2, protocol §6.1, §6.3, §14.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
cleanup
- node: only a wrong code counts towards the join lock, now per account
(5) as well as node-wide (20), and it is consulted only when a code is
tried. Every member reconnecting gets the group key through join_request,
so a lock checked before recognition let one member refuse it to everyone.
- node: link previews read the body as a stream and stop at the cap,
counted on decoded bytes; a declared oversized image is not read; 15 s
total deadline; image decoding off the loop. `client.get` had buffered
the whole (decompressed) response before the caps looked at it.
- hub: the daily purge of never-verified accounts detaches their IP-log
rows (keeping the name) and clears every other reference first, and each
cleanup step runs on its own. On PostgreSQL the bare DELETE violated the
ip_logs foreign key and stopped every purge behind it for good.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
The interface reads a member's cap from transfer_state and never read the
copy on the ack. The transfer probe reads it from transfer_state too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
A node hosting a public group syncs the hub's blocklist on every
connection (paged, node token only) and applies pushed changes. A
blocked file leaves the index and is refused (content_blocked); private
groups are untouched. The unused per-hash check route is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
Any member could make a node hold unbounded challenge requests; a
connection now keeps at most 8, 64 KiB each. root_add, group_attach,
invite_create and tmdb_config signed less than they did; their subjects
are now canonical JSON of every value (the TMDB token by SHA-256).
MNP 5.0, floor kept at 4.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
serialize/deserialize had no production caller, and took with them the
per-chunk signature, the ChaCha20 cipher variant and the zstandard
dependency. Key derivations are unchanged. Docs corrected, including
design §4.3's claim that chunks are compressed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Every node the 4.0 floor admits signs its challenge, and one without a
channel binding could not complete the proof anyway, so a missing
signature is refused like a wrong one (browser and QUIC client).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
TMDB fiches are fetched lazily, on browse, and the fetch used to run whatever
the moment it was first triggered — routinely before the operator had opened
settings and picked a language, so it queried in TMDB's English default. Then
the fiche was cached by tmdb_id alone, with no note of language and a 30-day
TTL, so switching to the intended language afterwards changed nothing: the
English fiche was served until it expired. The operator's only recourse was to
find and wipe the cache by hand (found live 2026-09-26: a whole library indexed
in English although "Français" had been chosen).
Two rules now, both there to make the first fetch the right language rather
than English-then-corrected, and to stop the doubled requests that eventually
get a node rate-limited:
- No language configured, no query. media_meta_req/season_meta_req answer
confidence 0 and make no TMDB call while tmdb_language is unset; the fetch
waits for the operator's choice, so the first (and only) query is in it.
English is now a first-class choice (en-US), not the default of skipping the
setting.
- Changing the language wipes the metadata cache (ops.set_tmdb_config), so the
new language takes effect on an already-browsed library. The file->tmdb
matches are language-independent and kept. The client refetches on the
tmdb_config_ack that carries the new language, so the grid updates without a
page reload.
docs/MESHBAY_DESIGN.md §9.7 states both rules; tests cover the gate and the
cache wipe, and two existing handler harnesses now declare a language.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The audience split stopped a member's node credential from opening the hub API.
It did not stop the credential being *replayed to another node*: the MNP token
carried the member's whole group set and named no node, so a token handed to
node A's operator could be presented to node B the member also belongs to. That
does not read content on B — the handshake still requires proving node B's group
key, which the operator lacks — but it reaches B's pre-proof window and fetches
the member's *encrypted* keypair bundle for B (offline-attackable, bounded,
audited): a disclosure §2.4 says should not follow from hosting a member on A.
The token now names the node it is minted for (a `node` claim = that node's
Ed25519 key), and authorize_token refuses one that names a different key. The
client already knows the target node's key (from /v1/groups/{id}/nodes) and asks
for a token bound to it: POST /v1/nodes/mnp-token takes node_pk, and
transport.connect threads it (group-page, the connection pool and rewrap pass
n.pk_node; reconnect preserves it). A token that names no node is still
accepted, because the hub only mints one for the authenticated requester, so an
unbound token grants nothing across accounts — which also keeps non-binding
callers working with no churn.
Done before deploy, so it folds into the MNP 4.0 flag day rather than needing
its own. Docs: §5.2, register E10, MESHBAY_NODE_PROTOCOL.md §6.3.
test_handshake.py and test_mnp_token.py hold the binding (a token for node A is
refused by node B, accepted by node A; an unbound token still works); red
before, green after. common/node/hub suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
Each section of ops.py becomes a module of meshbay_node/ops/ (core,
node_toml, members, chat, groups, roots, files, settings, apps), cut as
text; ops/__init__.py keeps the docstring and re-exports every name, so
`ops.<name>` is unchanged for every caller. Logger name unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
_do_admin_response looked each operation up in a 28-branch elif, every
branch the same call. The admin cases of the dispatch golden are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
The pre-authentication guards stay explicit code, in the same order and
text. After the handshake, a table maps each type to its handler and to
whether it runs as a task, the choice each branch made; the three inline
blocks become StreamingMixin methods, unchanged. The dispatch golden is
identical, including types that are not strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
DispatchMixin in transport/webrtc/dispatch.py, with the pre-proof fetch
bound it enforces. The elif chain moves as it is; turning it into a table
is the next commit, on its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
SessionCore in transport/webrtc/core.py, last among the bases: state,
the data channel, task ownership, the peer registry, sending and teardown.
The facade keeps _dispatch_message and WebRTCTransport.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
webrtc_server
AdminMixin in transport/webrtc/admin.py: who the operator is, the signed
challenge, signature checks, and _do_admin_response moved unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
GroupOpsMixin in transport/webrtc/group_ops.py: member revocation and
unpinning, the group key rotation, apps and their directories with the
allow-list, and Search listing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
NodeOpsMixin in transport/webrtc/node_ops.py: status and settings, roster
and denylist, roots, hosted groups, reload, scan pacing and transfer
limits, with their class-level bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Three headers stayed in webrtc_server when their sections moved; they now
sit above the same methods in blobs.py and admission.py. Two that were only
a title over what is now a whole module are dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
HandshakeMixin in transport/webrtc/handshake.py: challenge, proof, channel
binding, and the sealed configuration a peer receives once admitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
The peer-session ceiling no longer quotes the hub's old per-account number,
and the note on a single uploads/ directory goes: its constant left with
f3fb449, when an upload started landing in the folder it was sent to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
AdmissionMixin in transport/webrtc/admission.py, with the invite handle
pattern and the join attempt bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
UploadMixin in transport/webrtc/upload_handlers.py, with the upload cap;
_upload_chunk moved unchanged, _append_chunk joins webrtc/disk.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
TransferMixin in transport/webrtc/transfer_handlers.py, with the sweep
interval.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
FilesMixin in transport/webrtc/files.py; the blocking disk helpers join
_locate in webrtc/disk.py, and the lease states go to webrtc/limits.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
ChatMixin in transport/webrtc/chat.py: sealed messages, history, epoch
keys, link previews with their cache and rate bounds, and the operator's
chat ops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
BlobsMixin in transport/webrtc/blobs.py, with the blob caps and the kind
pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
StreamingMixin in transport/webrtc/apps/streaming.py: stream credit,
handover, the transcode slots and _stream_video_inner, moved unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
VideoMetaMixin in transport/webrtc/apps/video_meta.py: media and season
meta, TMDB search and its per-member bound, posters, and the operator's
TMDB ops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
| |
An operator op that exists for one app lives with that app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
MusicMixin in transport/webrtc/apps/music.py: tags and cover art, and the
audio transcode with the extension list that gates it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
SubtitlesMixin in transport/webrtc/apps/subtitles.py; _locate, which it
shares with the files, music and streaming handlers, in webrtc/disk.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
transport/webrtc/channel.py, limits.py and media_tools.py, cut from
webrtc_server.py as text; the facade imports them back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
connectionstatechange dropped a closed or failed session from the
transport and stopped its tasks, but only close() took it out of the
group's peer set, and nothing on that path called it. Every broadcast
then went to closed channels, and each reconnect left a dead session
held until restart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A phone on 4G found one of five groups missing from Search on half its
visits, and all but one on a fifth of them. Every offer that reached the node
was answered in about a second; the hub refused the others with 429 — 93 of
them in half an hour on meshbay.org, all from that phone, all to nodes that
were up. Two ceilings did it. Three pending offers per account was exactly
what one Search page dialled at once, so the first tile or reconnection beside
the sweep was refused. Thirty offers a minute per address and per node was
spent by four or five reloads of a page whose groups share one node.
Sized now against an account with twenty groups on three devices:
- 32 offers pending per account, across devices and nodes.
- A budget per account and per node, a burst of 120 refilled at two a
second. This is what bounds a member's cost to one machine (H6), and it
leaves their other nodes alone. Counted by account, because a mobile
carrier puts many subscribers behind one IPv4 address.
- 600 a minute per address and per node, as a coarse guard in front of
authentication only.
Both refusals carry Retry-After, which the browser now honours. The node's
own ceiling on peer sessions is unchanged; its comment no longer quotes the
old per-account number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
The plan was a working document; what it decided lives in
MESHBAY_DESIGN.md and MESHBAY_NODE_PROTOCOL.md. Code and tests now
cite those instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
New invite kind "link": member of one group, once, never operator, not
spendable by an active member, capped at 20 per group, cancellable by
handle. Signed ops invite_link_create / invite_cancel, loopback routes,
and the known-device join path now accepts a link code. Adds the plan,
docs/invite-links.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
node_pk in handshake_challenge is now signed over the channel binding
and both nonces, so a client can check the node key before a join
rather than only at the ack. Both transports; the browser and the QUIC
client refuse a wrong signature and treat an absent one as an older node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
A browser limits bytes, so a bound in seconds had to be sized for the
highest-bitrate file and every ordinary one then held a fraction of what
the same buffer would have taken. Floored at the old 90 s so nothing
pulls less than before, and walked up rather than declared. A refused
append now waits for an eviction instead of retrying on every tick.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The pass before this applied ruff's own fixes. These are the ones needing a
decision, and the point of doing them is that `ruff check .` now passes: a
linter reporting 98 known-acceptable findings reports nothing, because the next
real one arrives invisible.
**Lines over 100 (70).** Mostly wrapped where they stood. Two exceptions: the
aligned trailing comments in `protocol.py`'s message table were shortened rather
than wrapped, because wrapping one row of a table breaks the table; and in
`models.py` the column comments moved above their columns for the same reason.
**Imports below the first statement (14).** `csam.py` kept its FastAPI imports
under a section header halfway down the file; two node tests had a constant and
a `pytestmark` wedged between two import blocks. Moved, not suppressed.
**Bindings nothing reads (4).** Three in tests, where the call stays and only the
name goes — `_user(client, "listener")` is there to create the user, not to
return one. The fourth was in `revocation.py` and was not a lint finding at all:
`_connect_and_listen` opened an httpx stream to the WebSocket URL, did `pass`,
and then opened the real connection through the `websockets` library. One
pointless request per connect, left over from before that library was used
directly. Removed, and `httpx` with it.
**`l` as a name (4)**, **semicolons (6)** in the POC spikes, and the rest.
2893 passed, the same count as the two commits before it.
`meshbay_node/revocation.py` is worth a decision separately: 154 lines that
nothing imports, superseded by `hub_client.maintain_ws`'s `on_revocation`. This
commit only stopped it failing the linter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`ruff check .` had gone unrun long enough to report 568 errors, which is the
same as having no linter: the next real finding would have been invisible in the
noise. This is the 521 it fixes by itself, in 173 files, and nothing else — the
98 it cannot fix are the next commit.
What actually changed: import sorting (225), imports nobody used (87, none of
them a re-export — no `__init__.py` is touched, which was the one way this could
have broken an import elsewhere), `datetime.timezone.utc` to `datetime.UTC` (69)
and `asyncio.TimeoutError` to `TimeoutError` (18), both plain aliases on the 3.12
this project requires, `Optional[X]` to `X | None` (24), and f-strings with
nothing to interpolate (19).
Checked rather than assumed: every module in the three packages still imports,
and the suite is 2893 passed — the same count, test for test, as the merge
before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |\
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
One conflict, in §15.3's open list, and it was two changes agreeing rather than
disagreeing: this side removed the rows for the third-party search bound and the
node-announcement bound because both are now built (AV27, AV28), while the other
side kept them and added a new one. Resolved by keeping what is genuinely still
open — per-device revocation having no CLI — and leaving the two closed.
`webrtc_server.py` merged without conflict but the two sides met inside one
function: `_upload_chunk` gained the operator's `max_upload_gb` ceiling from
there and the per-group lock and `off_disk` calls from here. Read back rather
than trusted: the operator's ceiling now sits inside the critical section that
keeps chunk ordering, and the unlink beside it goes to the disk thread with
everything else.
2893 passed. The twelve `test_sticky_header.py[firefox]` setup errors are the
open Firefox on this machine, as before.
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Was a 4 GB constant in webrtc_server.py, the same on a small board and on
a machine holding a library. Now max_upload_gb in node.toml, on the Node
page and via `meshbay-node transfers max-size`, read from the transport
context per chunk so a change reaches an upload already running.
MESHBAY_DESIGN.md §6.4; §15.3 records a defect found beside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|