| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A design discussion on 2026-08-17 settled Phase 13 and, in doing so, changed
four things the spec states. v6 restates only those; v5 stays authoritative for
everything it does not touch, per the convention v5 itself used with v4.
What changed:
* The native shell is **Electron**, not pywebview — structural decision 18
reversed. The SPA depends on Chromium-class APIs (WebRTC, WebCrypto
X25519/Ed25519, MSE, Service Workers), so keeping Chromium keeps
transport.js, crypto.js, keyderive.js, downloads.js and sw.js *as the
client*. A system webview meant reimplementing ~2500-3000 lines. The old
"69 % reused" figure was measured against an app.js of ~2600 lines; it is
4586.
* A group's content is **several named roots**, not one directory, because
the planned video and audio libraries will not live in one folder on one
disk.
* **Device linking**: one person may hold several devices on a node,
admitted by a key the node already pinned and bound by a one-time code the
new device generates. Without it a native client is refused where a browser
is not, and an account created natively could never be opened in a browser.
* **Authorship is authenticated, not asserted** — chat senders sign, uploads
have a provable owner, and delete authorization moves from the uploading
key to the account.
And one rule v5 assumed without writing down: **group-related server state
lives on the node.** Verified for multi-root — SwarmSource carries hashes and
endpoints, no paths.
Also here: the Caddy configuration, which was a snippet in the roadmap that
would have broken the SPA (it predates /a/<hash>/ asset versioning and would
have 404ed /sw.js, silently killing streamed downloads on Firefox and Safari);
and downloads.html, which becomes a security page once a release key exists.
Phase 15 was re-read against device linking and is wrong as written: a sender
key must be per **device**, never per person, or two devices sharing a chain
produce key and nonce reuse — C1 again, one level down. senderkeys.py already
fails this silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
draft-v5 §7 rewritten around the keypair bundle, because that is where the last
open finding actually lives. New §7.1 states the adversary (an operator holding
their own node's disk), what cracking a bundle yields (identity keys, hence
content on *other* nodes and the ability to sign as that user — not the content
they host in the clear by design), and the measured numbers rather than
adjectives: PBKDF2 241 ms vs Argon2id 88 ms natively, a GPU ceiling moving from
~8k to ~2k guesses/s, six days for a 10⁹ dictionary run, four random words
outlasting the sun. The honest summary is in there too — a factor of four on one
card, not a thousand; what it buys is the cost of scale.
§2 gains the row the table never had: **your identity keys stay yours**, ⚠️
against a malicious node operator. An operator hosts your content by design, and
that was documented; that they can also try to become *you* was not. That is the
difference between reading what they host and reading what other operators host.
§4 records that the challenge now carries `node_pk`, why (a first-time member
signs a transcript naming the node and has no GEK to complete a handshake with),
and that it is checked against the ack rather than trusted. Also that refusals
carry a code, and what `not_a_member` usually means.
§8.1 states the multi-browser property plainly — one identity across browsers,
recovered with the passphrase, no second code — together with its cost, since it
is the same mechanism as C4.
invite-pairing-v1 is no longer "a proposal": it shipped. §9bis gains the four
browser-found failures and their common thread — e2e.py is a second
implementation of the client, written in the right order by construction, so it
proves the protocol and nothing about app.js.
CLAUDE.md gets the two things a future session must not rediscover the hard way:
the KDF parameters live in three places held identical by a parity test, and an
unbounded await on the hub socket makes a node silently unreachable (three found).
second-review: C4 marked reduced, not closed. devel-phases-next: 12.2's CSP must
keep `wasm-unsafe-eval`, or the strict policy locks every user out of their keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
draft-v5 §2: against an active hub, reading content moves from "❌ H3" to
"❌ T3 (browser) · ✅ native". The defensible sentence becomes "the hub cannot
read your content unless it ships you malicious client code" — T3 is now the only
path, it is an artifact rather than a silent directory lie, and it does not exist
for a native client. New §5.5 describes admission and key delivery, with the four
properties that carry it and the one exception (open-join groups, where the hub
can walk in the front door — a property of open joining, and the setting is read
from node.toml).
Corrected while writing it: §5.1 said the C5b fix stopped a group admin who does
not run the node from inviting, and that the redesign reverses this. It does not,
because delegation was deferred. What changed is the timing — the operator issues
a code and is then out of the loop.
devel-phases-next: 12.1 is done and NOT as written. The plan was key transparency
plus safety numbers; what shipped removes the directory read instead. Safety
numbers make substitution detectable by a human who checks, at first contact,
when there is nothing to check against. 12.2 (served-SPA integrity) is now the
highest-value item in that phase. Phase 14 marked for what landed.
second-review: H3 and M3 annotated closed at the finding, with what actually
closed them. The §7 verdict table is left intact — it is the record of an audit
on a date, and falsifying it would be worse than leaving it — with a note
pointing at draft-v5 §2 for current state.
CLAUDE.md matters most here, being loaded every session: NS4 read "admin_pk_ed25519
auto-pinned from keystore ✅ DONE", which is M3 described as a feature. Rewritten,
with the two fixes that must never be attempted (auto-pin, hub lookup).
QE/deploy/README.md: set-admin-pk retired from the walkthrough; the regression
checklist now exercises pairing, joining by code, recognition without a code, and
revocation.
USERGUIDE.md is beyond the invite work but was actively wrong: it told users to
POST GEK bundles to a hub endpoint deleted in Phase 12, and to re-wrap for every
remaining member on revocation. Both replaced with what the code does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every operator action lived behind a web UI on the node's own loopback
interface. For the normal deployment — a node on a server reached over SSH —
that is unusable: no browser on the host, and 11.5.3 added a per-run token that
had to be copied out of a log to get in.
status hub, node public key, daemon state, groups, admin-key pinning.
Reads the keystore directly so it works while the daemon is STOPPED,
which is exactly when it is needed: the daemon cannot stay up before
its key is linked or before a group exists.
ui prints the URL and the ssh -L line. It does not open a browser —
that was an assumption about the environment, and a wrong one.
gek-init initialises a group key through the daemon's loopback API. Same
operation as the admin UI button, no browser involved.
Also fixes a latent bug in QE/deploy/deploy-node.sh: the pkill pattern was
unanchored, so it matched any shell whose command line merely mentioned the
daemon — including the one running the script. It killed a session three times
before being pinned down. Anchored to the end of the command line.
Verified against the live deployment. grenet and cbesson both connect over
WebRTC through real NAT and can browse, download, stream, upload and chat. The
node audit log confirms the security properties in production: uploads land in
.uploads/{user_id}/ (C5a), the invite required the operator's signature over an
admin transcript (C5b, H5), the pre-proof bundle window is bounded and audited
(C4), and a non-member handshake was refused.
Docs updated: Phase 14 marked partially delivered with the reason, draft-v5 §5.3
records the two operator personas, QE/deploy/README.md documents the commands
and the remaining browser-only gaps (invite, delete).
Tests: 121 node.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Investigated aioquic 1.3.0 before implementing the QUIC challenge/response,
since the binding anchor gates the whole design.
No RFC 5705 exporter exists (aioquic.tls.Context has no export_keying_material),
so the preferred anchor is unavailable. Certificate access is asymmetric: the
server reaches its own cert via the public tls.certificate, but the client can
only reach the server's via tls._peer_certificate — a private attribute, behind
a QuicConnection that exposes no tls accessor at all.
That matters because binding a security check to a private API means an
upgrade can remove it silently. Since make_proof() refuses an empty binding
(11.5.21), a rename would fail loudly rather than degrade — but only while the
refusal path stays strict.
Three options recorded with a recommendation: certificate hash via the private
attribute with a guard test that fails CI on upgrade, plus pinned aioquic;
or bind to pk_node instead, which for QUIC may suffice since signaling is not
hub-relayed — but that requires certificate pinning, as the QUIC client
currently does not verify the TLS certificate at all; or upstream an exporter.
No implementation started: the challenge/response needs both protocol sides,
test updates in two files and multiple verification cycles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Supersedes draft-v4, which described a system the code did not implement and
made several claims that were simply wrong — "ALL operations require the GEK
proof" (true on one of four transports), "Argon2id 256 MB" (hub only), "hub
stores no content metadata" (private file hashes were registered with it).
Written as a delta over v4: sections not restated are unchanged. Carries an
explicit rule — a claim must name the adversary it holds against — and a
per-adversary table replacing v4's informal assurances.
Records the decisions: transport (aiortc primary, QUIC retained, TCP and the
node HTTP API removed), unified handshake with mutual authentication, admin
operation transcripts, node authority over GEK storage and activation, upload
confinement, hub node-registration and signaling authorization, and the client
architecture — hub keeps serving the web SPA, native client offered alongside,
hub minimization deferred.
States plainly what is NOT true. The defensible claim is "the hub cannot read
your content unless it actively attacks you", not "unreadable by other parties,
even the hub": H3 (hub is the key directory and can substitute a key at invite
time) is open until Phase 12.1, and T3 (hub serves the SPA) is accepted
permanently by decision. Content is also readable by every group member and by
the node operator, so "end-to-end" here means client-to-node, never
client-to-client.
Corrects the v4 NAT traversal account: punch_nat() is a single UDP probe with
no STUN, no candidate gathering and no fallback, validated on one ISP. ICE is
the traversal path, including for native clients.
Open items listed with status, including C6 on the QUIC path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Operator decisions (tmp-decisions.md D1/D2/D4):
- the hub keeps serving the web UI (zero-install path stays)
- a native desktop client is offered ALONGSIDE it, not as a replacement
- hub minimization is off the critical path and may be dropped
Phase 12 was "Hub minimization: registrar and nothing more". Most of it is
dropped: route-inventory blindness test, opaque private-group metadata,
chat_notify metadata minimization, residual schema cleanup. The swarm item
already shipped in 11.5.18.
Two items are kept, because the decision makes them more relevant rather than
less — the hub stays in the trusted path by choice, so what it can substitute
and what code it serves both still matter:
12.1 key transparency + safety numbers [H3]. This is the last open High
finding and nothing else fixes it: the hub is the public key directory,
so substituting a key during an invite hands it the group key silently,
with no JWT forgery and no code injection. Dropping Phase 12 wholesale
would have left it open indefinitely.
12.2 served-SPA integrity: CSP, SRI, and a hub-published signed digest of the
bundle so a native client can verify what the browser was given.
12.3 honest labelling of /app/ as the hub-served path.
12.4 written threat model — the thing that stops the overclaiming pattern.
Recorded consequence: T3 is now accepted permanently for browser users. A hub
that serves the code can exfiltrate keys from the page whatever the protocol
does. The claim that still holds, and that the docs should make, is "the hub
cannot read your content unless it actively attacks you" — not "unreadable by
other parties, even the hub".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Second architecture and security review (second-review.md): 6 critical and
7 high findings against the Phase 12 implementation, plus an assessment of
whether the system meets its end-to-end confidentiality claim.
Roadmap rewritten against those findings (devel-phases-next.md): new blocking
Phase 11.5 (security remediation), Phase 12 (hub minimization), Phase 13
(native desktop client). Old phases 12-17 renumbered to 14-19.
tmp-decisions.md records two open decisions: whether the hub keeps serving
the web UI, and browser extension vs native desktop client vs both.
CLAUDE.md and devel-phases-next.md also carry pre-existing Phase 12 edits from
the working tree that could not be cleanly separated from the review changes.
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>
|
| |
|
|
|
|
|
|
|
|
| |
packaging)
Phase 11 (node daemon production-ready) is the critical next step — all
WebRTC, WS, chat, and HTTP wiring currently lives in the demo script
and must move into the daemon.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
Replace download-then-play VideoPlayer with MSE (MediaSource Extensions)
streaming. Node remuxes to fMP4 via ffmpeg, probes codecs with ffprobe,
and sends encrypted segments over DataChannel. Browser decrypts and
appends to SourceBuffer — playback starts within seconds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
search)
Six self-service features for the web SPA:
- Group creation UI with GEK auto-generation (AES-256-GCM ECIES)
- Member management + invite by username (GEK wrapping for invitee)
- Open group self-join flow (POST /v1/groups/{id}/join)
- File upload client→node (FILE_UPLOAD MNP type, .uploads/ staging)
- IndexedDB caching of group file indexes (instant display on revisit)
- Cross-group file search (SearchPage, pure client-side on cached indexes)
11 new tests (166 total): 8 group self-service + 3 AES GEK wrap/unwrap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
- 10.5: Notification model + CRUD API (list, mark read, mark all read)
Triggered on: group invite, role change, suspend/unsuspend
- 10.6: SettingsPage shows role, per-group notification mute (localStorage)
- 10.7: GET /v1/groups?q= search filter (ilike on name)
- 10.8: NotificationFeed on home page + bell with unread badge in navbar
- 10.10: GET /v1/hub/version endpoint for client update checks
- 8 new tests (test_notifications.py), 155 total
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Site overlay: landing page, /about, /downloads (dark/light, responsive)
- User role column (user/moderator/admin) with config-based admin sync
- require_moderator dependency + admin API (8 endpoints: stats, users,
groups, audit logs)
- Admin SPA panel at #/admin with 5 tabs (stats, users, groups, logs,
blocklist) — visible only to moderators/admins
- SPA also served at /app/ for Caddy site overlay integration
- GET /v1/users/me returns current user role
- 15 new tests, 147 total passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
| |
Multi-group: single QUIC port (multiplexing), group_id from JWT.
Signaling punch/connect: hub WS client_incoming/punch_ready protocol,
reduces handshake 12.7s → < 200ms. SFR Port-Restricted findings added.
Chat model: between forum and Signal — persistent, threaded, E2E,
per-group scope, push for online / pull for offline members.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
|
|
Phase 7: Node v2 (multi-group, 0-RTT, HLS QUIC, chat)
Phase 8: Hub v2 (admin roles, MHP network, CSAM)
Phase 9: Android MVP (Kotlin, quiche JNI, STUN)
Phase 10: Web client v2 (private group AES-GCM, HLS player)
Phase 11: Network resilience (TURN relay, 0-RTT, CGNAT)
Phase 12: RPM/DEB CI/CD
Open questions per phase documented.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|