| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The node-audience token (previous commit) is a change to what a peer must
present, so it is a MAJOR per the versioning rule (§5.6): a pre-4.0 client
presents its hub session token and a 4.0 node refuses it, and there is no
compatibility branch, because leaving one would keep a hub credential reachable
by every node (C6's lesson). So the floor moves with the version.
- MNP_VERSION 3.4 -> 4.0 and MNP_MIN_SUPPORTED 3.0 -> 4.0 (meshbay_common);
transport.js MNP_V/MNP_V_MIN -> 4.0 to match.
- MIN_CLIENT_VERSION 0.13.0 -> 0.16.0 so a stale desktop client is told to
update before connecting rather than meeting a handshake refusal it cannot
read; the browser reloads this build from the hub.
- Regenerate tests/golden/dispatch.json: the only change is the `v` the node
stamps on outbound messages, 3.4 -> 4.0 (56 cases, v field only).
- Document the split and the flag day: MESHBAY_DESIGN.md §5.2 (the handshake
token is the MNP-audience token), §5.6 (the 4.0 flag day), register E10 and
decision 23; MESHBAY_NODE_PROTOCOL.md §6.3 (authorize_token binds MNP_AUD)
and the wire-version banner.
Deploy is coordinated and atomic (common+hub+node+SPA together); a live
browser-to-node validation and the deploy itself remain. common (173), node
(1489, the pre-existing test_cli_golden argparse/prog artifact aside) and hub
(1471) suites all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
A link is redeemable by whoever opens it first, so it can be sent by any
messaging app. The address is optional (mail + label only); a link lives
7 days, fixed. Adds a Share button; see MESHBAY_DESIGN.md §3.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
transport.js keeps the core (connection, reconnect, leases, dispatch).
Chat, media, admin, upload and device methods move, cut as text, into
transport-*.js scripts that hand a class of their own to extendTransport,
which copies each method onto MeshBayTransport.prototype; the codec,
roster checks, node pins and the rewrap fan-out move as they were. Both
shells load them after transport.js. Every prototype member, class
property and top-level function has the same source text as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
apps.js registers Videos, Music, Photos and every settings pane through
lazy.js; the group page does the same for the video player and the
settings panel, and the shell for the search page. The first download
goes from 49 modules / 386 KB gzip to 37 / 289 KB; opening Music now
fetches music-app.js and media-tiles.js and nothing of Videos.
test_first_load_is_lean holds the eager graph; test_spa_imports checks
that every on-demand load names an export that exists.
Co-Authored-By: Claude Opus 5.5 <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>
|
| |
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
| |
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>
|
| |
|
|
| |
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>
|
| |
|
|
|
|
|
| |
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>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Mail to OVH redirections such as devel@meshbay.org never left the server:
Postfix listed meshbay.org in mydestination and delivered it locally, and
systemd-resolved answered MX queries for the hostname with an empty
authoritative reply. New §9 states both settings and how to check them.
§1 now shows the DNS records as deployed (IPv6, DKIM, DMARC quarantine,
PTR), §2 adds the IPv6 address to SPF and PTR, and §3 describes the
installation itself — packages, main.cf, DKIM key, OpenDKIM tables,
resolved drop-in — instead of pointing at a script outside the repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
dead node
The other half of the 4G failure. The page negotiated every group twice: the
sweep opened a connection, read the index and closed it, then the warm-up
opened the same group again. The sweep, the warm-up and the tiles each had a
concurrency ceiling of their own, and together they went past what the hub
admits per account. Whatever the hub refused was then reported as "node
unreachable" and remembered as down, which put that group last next time.
- Every connection goes through ConnectionPool, which holds the page's one
ceiling (six at once, sized for twenty groups on a phone) and keeps what
the sweep opened for the tiles. A visit costs one offer per group. A
refresh costs none for a connection that answers a four-second ping, and
a connection that died while the phone slept is replaced, not waited on.
- A connection whose index is being read is held against eviction. With
more groups than the pool keeps, it was otherwise the least recently used
one.
- Negotiations still under way when the page closes close what they get,
and a sweep cut short that way remembers nobody as down.
- transport.js sends an offer again on 429, 502 or 503, honouring
Retry-After, with jittered waits of about twenty seconds at worst. Search
counts each retry as progress. A 404, 403 or 504 still fails at once, so
a dead node costs no time.
The fan-out tests assumed a ceiling of three and were re-measured: four dead
groups of twelve now hold nothing back, even on a first visit. The pool and
the retry run as shipped code, lifted as text, against a fake clock. Each
guard was checked by removing it and seeing its test fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The list under "Invite by link" answered every ticket the group had ever
minted, so a link that somebody had already used sat there saying "used by
<name>" for the thirty days of KEEP_REDEEMED — beside the member row it had
just produced, and above the links that still wait for somebody, which are
the only ones there is anything to do about. The node's own `member list`
had never shown them: it selects `used_at IS NULL`.
The listing now selects `redeemed_by IS NULL`, and drops the `redeemed`
status and the `redeemed_by` field with it. The row itself still lives for
KEEP_REDEEMED, which is what lets a reload or a second tab of the invitation
page be answered rather than refused; its comment says that now instead of
naming a list it is no longer in.
The SPA filters too, because the desktop client's copy of this interface can
be newer than the hub it is signed into.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
The per-recipient cooldown was shared by every purpose, so the code a
person asked for by registering within two minutes of an invitation
link was refused, silently. The cooldown is now per family (invitations
vs the account's own steps); the daily cap still counts everything.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
The security claims table, §3.4's property 2, the protocol's stated
limits, the quickstart and the user guide's defaults now say where a
link's code differs: bound to its account only when redeemed, and held
by the hub when the inviter asks it to mail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
The CLI makes both halves itself — the node's code, then the hub's
ticket bound to the address — and prints the link; a refused ticket
takes the code back, and cancel takes back both. The CLI never asks the
hub to mail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
#/invite takes the link out of the address on load and keeps it in the
tab through registration and sign-in; joining is one click, only the
ticket goes to the hub, and the code goes only to the node the link
names once it has signed its challenge. Members tab gains "Invite by
link" (shared e-mail box, pending list, cancel both halves); home page
takes a pasted link. Browser probe drives the real app, signed out and in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
group_invite_links holds sha256(ticket) and the invitee's address blind
index; redeeming grants membership to that account only. Owner-only
create/list/cancel (a node token may create, never mail), 20 outstanding
per group, optional mail written by the hub itself and capped at 10 per
sender per day (mail.invite_link_daily_cap). MESHBAY_DESIGN.md §3.4 now
carries the whole link design.
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 "Send the invitation by e-mail" box under the Invite member field,
checked by default and stored as the invite_email preference. Unchecked,
invite-notify is never called and the hub never sees the code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
The toolbar's actions on the row under the pointer, sharing one action list
with the toolbar — which keeps showing what does not apply, disabled, while
the menu leaves it out. A count only where more than one item is concerned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
A native confirm() or alert() leaves the desktop client unable to type
until the window is refocused. ask.js draws both in the page; the SPA
test now bans all three browser dialogs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
`ops.revoke_member` and `ops.unpin_member` log nothing to audit.db while
the MNP handlers doing the same work audit it. Parked in §15.3: a
removal from the node page or the CLI leaves a refusal with nothing to
explain it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
A reconnection restarted the stream at the playhead, and that empties the
source buffer — spending the whole read-ahead at the one moment it was the
thing carrying the film. It carries on from the end of the buffer instead,
except where the buffer is short, the stream already ended, or a cast is
active: the receiver is fed from the wire and never had that buffer, so
resuming there would skip it forward by the lot.
Co-Authored-By: Claude Opus 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>
|
| |
|
|
|
|
|
|
|
|
| |
A member row appears only when a code is consumed, so revoking someone
invited to the wrong group was refused for having no row — and the node's
refusal aborted the browser's removal before its hub half, leaving them a
member everywhere with a live code. Revoking now cancels unredeemed codes
for that group, and a node refusal no longer cancels the hub removal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
QUICKSTART Step 4 named the wrong page. Recorded in §15.3 alongside it: the
key is read from `meshbay-node status` or the desktop client and from nowhere
else, which is a decision rather than a gap — anyone linking a node is already
at a shell prompt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
media_cache held video_meta, photo_meta and thumbs; the audio tags lived
only in the in-memory IndexEntry. So every start re-read every audio file
the node serves, and until that pass landed it served an index with no
artist on any track — one the Music app cannot group. Over a real
6176-file library the pass costs 27.8s cold and 6.4s from audio_meta.
Only what the bytes decided is stored. The filename and folder fallbacks
still run live, or a renamed file would get the old name's answer; the
sibling-cover scan reads the folder, so it stays live too; and a read
that failed is not cached, or one bad read becomes permanent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The grid's unit is an album, so a track whose artist tag is empty was
drawn nowhere — while `empty` counted it and stayed false, so no message
appeared either. An untagged library rendered a toolbar over a blank
page, with every track one mode-switch away and nothing saying so.
It gets a card, the same shape the singleton folding already mints. No
cover is looked up for it, or for any album this file invented: the
release name is one the browser wrote, and the request cannot match.
music_untagged_probe.py renders the real grid and reads the page back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
_reconnectLoop re-did the handshake and nothing else, so a page kept
whatever it last saw until someone reloaded it. That is invisible until
the node restarts: it rebuilds its index from index_cache.db, which
holds no enrichment, and Music is the one app whose enrichment is
persisted nowhere — for the length of the re-read pass it serves tracks
with no artist, and the album grid drew nothing.
onReconnected was one slot the video player took on open and cleared on
close; it is a listener set now, and carries the fresh ack.
docs/MESHBAY_DESIGN.md §15.3 records the two defects found alongside and
not fixed here.
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.
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The daemon built the defaults dict for `Roster.node_settings` by hand and
left out `max_concurrent_downloads` and `max_concurrent_uploads`. Absent
from the dict, both resolved to None, were assigned back onto the config,
and the transport skipped them — so node.toml was parsed, validated, and
then replaced by `transfers.py`'s own 8. Invisible to anyone who left the
value at 8, which is the value the template suggests.
There were three copies of that dict and they all disagreed: node_status'
was missing those two and `max_upload_gb` besides. One builder now,
`config.node_settings_defaults`, and the resolver's key list is a class
attribute the tests hold it to, along with the writer's.
1429 passed against a baseline of 1423; the two new behavioural tests fail
with the builder reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Found by writing the user guide against the CLI rather than against this
document. §6.7 listed a `member device` verb nobody ever wrote; §9.9 still
called several folders a Photos-only shape after app_directories gave every
application a set; §6.8 said five settings and the Node page shows eight;
§5.5 counted one transfer pool where there are three.
The per-device revocation gap is real and stays open — §15.3 names it as
the capability it is, not as a documentation error.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
QUICKSTART takes one Linux machine from downloaded packages to a working
group with a second member. USERGUIDE covers using a group and running a
node, for a reader who is not a developer. Windows stays in
PACKAGING-GUIDE.md.
Both are written from the code — the CLI, ops.py, the systemd units and
the interface catalogue — rather than from the specification, and they
state what is not built as readily as what is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
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>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
§16 opened by saying "code comments, tests and older documents cite sections
of the documents this one replaces", and closed by saying "nothing needs
editing to follow a reference". Neither holds since 20a8241: the code carries
no reference to a removed document, and that commit is precisely the editing
the second sentence said was unnecessary. Left as it was, that sentence tells
the next reader a dead reference may stay dead.
The table itself is unchanged and stays whole — every row still resolves
something. What changed is who asks: 629 commits of history that cannot be
rewritten, the superseded documents kept under docs/ (cast-smart-tv.md and
WINDOWS-PORT.md both route readers here by name), and notes held outside this
repository. The intro now says that, and states the rule the code follows
instead — cite this document and a section.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|