| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A client whose clock is a little fast could not connect at all: the MNP
handshake verified the hub-issued token with no leeway, so a token whose
`iat` was a few seconds ahead of the node's clock failed with "the token
is not yet valid (iat)". Seen against a freshly-resumed VM guest.
`meshbay_common.handshake.JWT_LEEWAY_SECONDS = 60` is the shared value;
applied to the handshake, the node's own hub-token decode, revocation-token
verification, and the hub's access-token decode. 60s absorbs NTP-level skew
without meaningfully widening the window on a stolen token (they already
carry a jti and an exp).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
Platform directories, signal handling, chmod guards, ffmpeg discovery,
and platform-conditional CLI messages — all testable on Linux.
See docs/WINDOWS-PORT.md §5 for the plan these implement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two bugs fixed:
1. `meshbay-node member invite <user>` created a local roster invite
but never told the hub to add the user to group_members, so the
group was invisible in the SPA. The node now calls
POST /v1/groups/{id}/members/{username} after creating the invite,
and the hub endpoint accepts node-scoped tokens (the admin_id
check is the real authorization guard).
2. The WebRTC handshake let a previously-pinned user reconnect without
a code even when a new invite was pending (e.g. after leave + re-invite).
Now any pending invite forces code entry, regardless of existing
member/pin status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Wizard (Electron):
- Auto-provisions node config (hub URL + username) from logged-in user
- node:start handles both cold start and restart of misconfigured daemon
- Waits for daemon to reach 'running', auto-links node key on hub
- probeNode accepts intermediate states for wizard progress feedback
Reset (meshbay-node reset):
- Unlinks node key from hub (DELETE /me/node_key, best-effort)
- Stops and disables daemon (systemctl --user disable --now)
- Erases ~/.config/meshbay, ~/.local/share/meshbay, ~/.local/state/meshbay
MusicBrainz contact:
- Resolved from owner's hub email instead of per-node roster config
- Removed musicbrainz_contact UI and WebRTC handshake field
- Removed set_musicbrainz_contact/musicbrainz_contact from roster
Node pairing:
- Added operator pairing banner on NodePage
- Added operator_paired flag to list_groups
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Create Group wizard (Electron-only) consolidates 6 steps across 4 interfaces
into a single multi-step page: group creation on hub, node attachment, root
selection via folder picker, GEK initialization, and auto-pairing — all in one
flow. Browser SPA keeps its current behavior unchanged.
Public group support (Option A — GEK for all groups):
- All groups have GEK regardless of visibility; open-join groups auto-admit
via TOFU when join_policy is "open"
- Key rotation blocked for public groups (API guard + UI hidden)
- Hub signaling allows WebRTC offers for nodes hosting open-join groups even
when the caller isn't a member yet
- attach_group writes join_policy to node.toml
- Daemon loads GEK for all groups, not just private ones
- Known-device path in join_request now auto-admits to open-join groups
Node loopback API bridge (Electron IPC):
- node:detect, node:call, node:pairing-code IPC handlers in main process
- Renderer never sees tokens, paths, or keys (session token = physical access)
- platform.js node namespace for UI consumption
- Loopback endpoints: roots CRUD, member-upload toggle, reload
Bug fixes:
- Root change detection: removed premature ctx["roots"] updates from add_root
and remove_root that prevented indexer retarget on reload
- Duplicate offline message: global fallback now gated on !group
- Signaling membership check: fallback to open-join groups for non-members
Sidebar groups sorted by last_activity_at (most recent first):
- New Group.last_activity_at column with Alembic migration
- POST /v1/groups/{id}/activity endpoint, called on connect and chat send
- Client-side sort + throttled hub updates (1/min)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Attaching a group to a node meant hand-editing node.toml with a UUID
copied from a browser URL, restarting, and knowing that gek-init exists.
Nothing in the CLI said so, and on a node reached over SSH there is no
paste buffer to carry a UUID across in the first place.
meshbay-node group add grenet --dir ~/grenet-share
The name is resolved against the operator's groups on the hub by the
daemon, which is the process holding the session. The [[groups]] block is
appended to node.toml as text rather than round-tripped through a TOML
writer: the file is hand-written and its comments explain decisions worth
keeping. The directory is created, and the command says what remains —
restart, then gek-init for that group.
It refuses a name it cannot find by printing the groups it can, with
their ids. That listing is the useful half of the answer and it was
missing everywhere: _daemon_api now renders an `available` list from any
endpoint that offers one.
The key is per group and pairing is not, which is the part that reads as
a gap until it is written down: one paired browser covers every group the
node hosts, while each group's key admits only its own members. §4 of the
user guide now says all three of those in one place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One keypair was copied to every node its owner joined, so cracking the bundle on
any single node yielded the identity used on all of them: their content on other
operators' machines, and the ability to sign as them anywhere. That lateral reach
was the part of C4 worth attacking.
Each node now gets its own keypair, generated the first time its owner joins it
and left with that node alone. An operator who cracks what sits on their own disk
holds a key that is a stranger to every other node — and on their own node, one
that unlocks nothing they did not already hold: they serve the content, the index
and every byte of it by design.
Nothing changes for the user. A first contact with a node already needed that
operator's code, and the key is created in the same step; a second browser still
recovers it from the node with the passphrase alone. Two operators can also no
longer tell they host the same person by comparing keys.
BREAKING, and deliberately without a compatibility path — the deployment is wiped
for the next demo:
- users.pk_ed25519 / pk_x25519 dropped (migration a7c31f9e40b2)
- registration no longer sends or stores a key
- PUT /v1/users/me/keys and regenerateKeys() gone; rotation is now
`member unpin` plus a fresh code, decided on the machine that pinned it
- /pubkeys returns an account id and a node's linking key. It was the directory
H3 read, and nothing wraps for it any more
- the pk_user JWT claim is gone
That last one closed a live defect the inventory turned up: the node recorded
pk_user as the uploader's identity and authorized deletion against it, so a hub
issuing a token naming its own key could delete anyone's uploads on any node.
Attribution now uses the key the node itself pinned.
A simplification falls out. Registration generates nothing, so a scripted signup
is a real account: `demo.py bootstrap` takes a wiped hub and node to a working
demo with no browser, which was impossible while keys were born in one.
Also fixes, found by running it on a wiped deployment: the key handed back on a
join now belongs to the group the connection is for, not the group named in the
invitation — an operator pairs node-wide but redeems the code while opening a
group, and expects to read it.
Tests: 343, including the two that state the property — a key pinned by one node
is refused at another, and someone else's code does not admit it. Verified end to
end against a wiped hub and node: bootstrap, pair, invite, join, download,
stream, second browser, revoke.
Design: docs/per-node-identity-v1.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Argon2id memory 64 → 128 MB. Memory is the lever, not time: it caps how many
guesses a card can hold at once, so the ceiling on one high-end GPU moves from
roughly 4k to roughly 2k guesses/s and its 24 GB fits ~187 lanes instead of ~375.
Measured through the vendored build: 640 ms, against 322 ms at 64 MB.
While measuring the real cost of a sign-in, found the SPA deriving the bundle key
twice — once for the key pair kept for the session, then again inside
decryptBundle() for the local bundle. At these parameters that is 0.6 s of pure
waste. Measured now, end to end:
auth_key (PBKDF2 600k) 239 ms
bundle v1 (PBKDF2 600k) 240 ms legacy, until every bundle is upgraded
bundle v2 (Argon2id 128MB) 650 ms
-----------------------------------
sign-in 1 129 ms (889 ms once no v1 bundles remain)
Once per sign-in, and only then: reopening a group, downloading, streaming and
reloading the page all reuse the key, which lives in IndexedDB from login.
Also bounds two waits in the node's hub WebSocket, found because the node went
silent again mid-deploy. It had reconnected after the hub restart, sent its auth
frame, and waited for a reply that never came — `ws.recv()` had no timeout, so a
hub that accepts a socket and then says nothing for a few seconds while starting
up parks the task forever: node running, logging nothing, invisible to everyone.
The auth exchange now times out at 15 s, connect at 15 s, and a refused auth
retries with a fresh token instead of ending the task for good.
QE harness signs in once per account and reuses the token — several clients there
stand for several browsers of one person, and what tells them apart is which keys
they hold, not which token, while the hub quite rightly rate-limits repeated
logins from one address.
Tests: 341, plus the live workflow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A node could be running, healthy in its own logs, and invisible to the hub with
nothing to say why. That is what "No nodes available" looked like from a browser,
and restarting the daemon was the only way out.
maintain_ws awaited the WebRTC offer handler inline, inside the loop that reads
the hub socket. One negotiation that did not finish — a client that closed its
tab mid-ICE is enough — stopped the node reading that socket at all: pings
unanswered, close frame never seen, later offers never served. The socket sat in
CLOSE-WAIT with the hub's goodbye unread in the receive queue, which is how this
was finally pinned down.
Offers are now answered in their own task, so the read loop keeps draining
whatever happens to any one peer. With that in place the existing reconnect logic
works: a hub restart is seen (1012), retried through the 502 while it comes back
up, and reconnected unattended — 19 seconds in the run that verified this.
Also:
- explicit ping_interval/ping_timeout. This connection is how a node stays
reachable, and a half-open socket looks exactly like a working one.
- a clean close ended `async for` without raising and reconnected in silence;
it now says so, because a node that stops being reachable should leave a trace.
- a failed negotiation logs the peer instead of taking the loop down with it.
Predates this branch (Phase 11), and independent of the invite work — surfaced
while testing it, because deploying the hub mid-session is exactly the trigger.
Tests: 232 node+common, plus the full QE/deploy/e2e.py run against the live
deployment after a deliberate hub restart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase 11.5.10.
POST /v1/nodes/announce accepted any pk_node with no proof the announcer held
the matching private key, so a user could register a node record carrying
someone else's node key, and records accumulated without limit.
The announcer now signs a domain-separated message binding the key to their
account — meshbay:node_announce:{user_id}:{pk_node}:{timestamp} — reusing the
shape already proven by /v1/nodes/auth, so a signature for one can never
satisfy the other. Same 60-second window.
Re-announcing the same key now updates the existing record in place instead of
creating a new row.
Three test helpers had to be taught to sign, which is the useful part: nothing
in the suite had ever exercised announce with an attacker's key. The new tests
cover the missing proof, a foreign key, a stale timestamp, and idempotence.
Note for the record: the node key is independent of the user's identity key.
Two hub tests asserted the announced pk_node equalled the user's pk_ed, which
happened to be true only because the daemon announces its keystore key. They
now assert against the announced key itself.
Tests: 157 hub+common, node suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Baseline commit capturing in-progress Phase 12 work that was already present
in the working tree (uncommitted) before the Phase 11.5 security remediation
begins. Committed as-is, without review or modification, so that remediation
changes arrive as a separable diff.
Contents: BundleStore (P2P GEK + keypair bundles), password split
(auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped
JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin
challenge-response, node local admin UI rewrite, browser key persistence.
Not authored in this session — captured to establish a baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
When watchdog detects file changes, the daemon now:
- Pushes INDEX_SYNC to all connected WebRTC peers in that group
- Registers file hashes with hub /v1/swarm/register endpoint
Also registers all file hashes on startup for initial discovery.
hub_client: add register_swarm() method for bulk hash registration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The node daemon was previously a skeleton that only started QUIC/TCP
servers and the local web UI. All browser-facing functionality (WebRTC,
hub WebSocket, chat store, HTTP file API) lived in QE demo scripts.
This rewrites daemon.py to be fully self-contained:
- WebRTC transport for browser clients (aiortc DataChannel)
- Hub WebSocket task (signaling, revocations, WebRTC offers)
- ChatStore per group (SQLite in ~/.local/share/meshbay/)
- HTTP file API per group (create_http_app on configured port)
- Graceful shutdown (all transports, stores, tasks)
- hub_client: _ws tracking + send_ws() for chat notifications
- config: data_dir field for persistent state
- systemd: security hardening (ProtectSystem, StateDirectory)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Complete browser-based client: Preact SPA with login, group file browser,
encrypted download, video playback, group chat, i18n, and dark/light theme.
Browser connects P2P to nodes behind residential NAT via WebRTC DataChannel
(aiortc). Hub handles signaling only — all data flows E2E.
Performance: pipelined downloads (8-chunk sliding window), binary msgpack
wire format (no base64), redundant I/O elimination. Large file downloads
stream to disk via File System Access API (showSaveFilePicker).
Validated on SFR + Orange residential NATs, Chrome + Firefox, IPv4/IPv6.
132 tests passing. Deployed to meshbay.org + Orange node.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Browser clients can now connect P2P to nodes behind residential NAT via
WebRTC DataChannel with ICE/STUN. Validated on SFR Port-Restricted Cone
NAT + 4G CGNAT across three scenarios (WiFi LAN, 4G IPv6, 4G IPv4 STUN).
No TURN relay needed. Hub serves only as signaling relay (<1 KB).
New files:
- webrtc_server.py: aiortc-based WebRTC transport (node side)
- signaling.py: SDP/ICE relay endpoint (hub side)
- transport.js: browser WebRTC client with msgpack framing
- webrtc-test.html: spike test page for browser→NAT→node validation
- test_webrtc_transport.py: 4 tests (handshake, file transfer, auth, guard)
- meshbay-draft-v4.md: architecture spec updated for web client
Modified:
- hub_client.py: WebRTC offer handling via hub WebSocket
- revocation.py: node_id from WS auth + webrtc_answer routing
- pyproject.toml: aiortc>=1.9 dependency
123 tests passing (117 existing + 6 new).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Implements all 8 milestones (7.0-7.7):
- 7.0: JWT carries `groups` claim; node verifies group membership at
MNP handshake (QUIC + TCP+TLS). Resolves security review C2.
- 7.1: QUIC 0-RTT session resumption via stored session tickets
(17-21ms reconnect vs 47ms cold).
- 7.2: Hub→node WebSocket signaling for NAT punch coordination
(`client_incoming`/`punch_ready`) + jti denylist push. Denylist
class blocks revoked users/jtis at handshake.
- 7.3: Multi-group daemon — one QUIC port serves N groups with
per-group GEK, shared_root, and index routing.
- 7.4: HLS streaming via QUIC (STREAM_SEGMENT message type, ffmpeg
segment extraction).
- 7.5: Sender Keys protocol for group chat (Signal Groups approach).
Each member has own sending chain key, HKDF chain ratchet, AES-256-GCM
encryption, Ed25519 signing. Resolves security review C1.
- 7.6: Chat store (SQLite via aiosqlite), CHAT_MESSAGE MNP wire type
with peer broadcast, web UI with WebSocket push.
- 7.7: Argon2id calibration CLI.
First security review included (first-review.md). 109 tests, demo-v3
validated against meshbay.org production hub.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
|
Register/login/announce/token-refresh/GEK-unwrap. JWT jti and
pk_user verified at login. Hub PK cached after first fetch.
6/6 tests passing with httpx.MockTransport (no network).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|