summaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
...
| * test: JS/Python transcript parity across the language boundaryChristophe Besson2026-08-131-0/+175
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The handshake proof and admin signature transcripts are built independently in crypto.js and in meshbay_common, and compared by producing identical bytes. Nothing on the wire carries the transcript — that is the design — but it means a one-byte disagreement between the two implementations is invisible to every other test while causing a total outage: no browser could complete a handshake with any node, and every file deletion would be rejected. Nothing else in the suite crosses this boundary. The 278 Python tests would all still pass. Drives the real crypto.js under node (stubbing window and crypto, which the module body touches but these functions do not) and compares against the real Python for the same vectors: both roles, short and empty group ids, non-ASCII group names and filenames — TextEncoder and str.encode must agree on UTF-8 — and field splits that would collide under naive concatenation. Verified to actually catch a mismatch rather than trusted for passing: removing one length prefix from the JS fails 6 vectors, and changing a single byte of the domain-separation prefix fails 6. crypto.js restored byte-identical afterwards. Skips when node is absent, which is a coverage gap rather than a pass — worth making a hard failure in CI (18.4). Tests: 168 hub+common. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * docs: draft-v5 — mark C6, M8 and 11.5.8 closedChristophe Besson2026-08-131-12/+29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5 is complete. All six critical and all seven high findings from the second review are now closed, bounded, or deferred by explicit decision. Updated in place rather than appended, so the document does not carry stale "open" markers next to shipped work: - §9 split into "closed since this document was drafted" and "still open", with C6, 11.5.6, 11.5.8 and M8 moved across and the closing mechanism recorded for each - §2 claim table: node impersonation is no longer pending - §3.1 QUIC now shows the unified handshake enforced - §4.2 records what the QUIC binding actually turned out to be, including the finding that a resumed TLS session carries no certificate, so the anchor travels with the session ticket - §4.4 states that the client pins pk_node and refuses a change Added a scope note: with C6 closed, pinning is defence in depth, not the primary control. A substituted node already fails the GEK proof; pinning covers the case where an attacker holds the group key and swaps the node underneath. H3 remains the last unfixed finding, and the document still says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * feat(client): pin node identities on first use — closes 11.5.8Christophe Besson2026-08-132-0/+24
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The client verified the node's Ed25519 signature but did not remember which key it had seen, so a substituted node was caught only by its lack of the GEK. Trust On First Use: the node's public key is recorded per node_id on the first successful handshake and compared on every later one. A change is refused outright — strict, per operator decision. A warning users can click through is decorative, and this is the SSH known-hosts tradeoff taken deliberately. Scope, stated honestly: with C6 closed this is defence in depth, not the primary control. A substituted node already fails the GEK proof. Pinning covers the case where an attacker HAS the group key — an ex-member, or a leaked GEK — and swaps the node underneath, which the proof alone cannot distinguish from the real one. Strict refusal needs an escape hatch or it is a dead end: a node operator who reinstalls and loses their keystore generates a new pk_node and would otherwise lock out every member. Settings gains a "Node identities" section showing the pin count and clearing them, with copy telling the user to verify out of band first. Also exposed as MeshBayTransport.clearNodePin() for the native client. Tests: hub+common green; all five static JS files syntax-checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * fix(hub): require proof of possession on node announce — closes M8Christophe Besson2026-08-135-13/+232
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
| * feat(quic): GEK proof and mutual authentication — closes C6Christophe Besson2026-08-133-8/+186
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5.4/5/6 — finding C6, the last open critical finding. QUIC ran a JWT-only handshake: a forged or stolen token reached the node and could inject chat without ever holding the group key. It now runs the same challenge/response as WebRTC through meshbay_common.handshake — client nonce, role-bound length-prefixed transcript, GEK proof, and the node proving itself with a GEK proof plus an Ed25519 signature over the transcript (C3). 11.5.6 channel binding, resolved by spike and then by two findings the spike could not predict: * aioquic 1.3.0 exposes no RFC 5705 exporter, and the peer certificate only via a private attribute. The server reads its own certificate from disk, so no internals are touched on that side; the client's access is guarded and fails loudly if an upgrade moves it. * A RESUMED TLS session carries no certificate — aioquic does not re-send it, so there is nothing live to bind to. The anchor therefore travels with the session ticket, which is sound because the ticket is cryptographically derived from the handshake where that certificate was presented. * The anchor had to travel with the ticket rather than live on the client object: resumption constructs a fresh client, so an instance-level cache was silently useless. Caught by the resumption test, not by inspection. Both paths refuse rather than degrade. No certificate and no cached anchor means the handshake fails; it never falls back to an unbound proof, which would silently drop MitM detection (L4). QuicChunkClient gains a peer_cert_der property and constructor argument, mirroring how session_ticket is already carried by the caller. Tests: 9 quic/multi-group, full node+common suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * docs: record 11.5.6 spike — QUIC channel binding constraintsChristophe Besson2026-08-131-1/+39
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
| * docs: draft-v5 architecture specChristophe Besson2026-08-133-2/+345
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
| * docs: rework Phase 12 — hub minimization deferred by operator decisionChristophe Besson2026-08-132-48/+69
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
| * refactor(quic): share the unified handshake authorizationChristophe Besson2026-08-132-25/+36
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5.4 — QUIC half. Findings M1, M9 on this transport. quic_server._do_handshake_sync was a second, weaker copy of the WebRTC logic: group_id was optional, so omitting it skipped the membership check entirely and fell back to the node's first group (M1); node-scoped daemon tokens were accepted as client tokens (M9); and the checks could drift from the WebRTC path independently, which is how they diverged in the first place. Authorization now comes from meshbay_common.handshake, shared with WebRTC. C6 IS STILL OPEN ON THIS TRANSPORT. There is no GEK proof here yet: a forged or stolen token still reaches the node over QUIC and can inject chat without holding the group key. What remains is the challenge/response and the mutual node proof — quic_binding() is written and unit-tested for exactly this, and 11.5.6 (whether a certificate hash is the right anchor, or an RFC 5705 exporter is reachable from aioquic) is still unproven. This commit narrows the gap to the proof itself; it does not close the finding. QUIC tests updated: default tokens are members of the test group, and clients pass group_id, since it is mandatory now. Tests: 9 quic/multi-group, full node+common suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * feat(mnp): unified handshake with mutual authenticationChristophe Besson2026-08-137-123/+680
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5.4/5/7/8 — findings C6 (WebRTC half), C3, L4, M1, M9. New meshbay_common/handshake.py is the single implementation of authorization and proof: JWT verify, scope, denylist, mandatory group_id, membership, hosting. The handshake previously existed three times over and only the newest copy enforced the GEK proof. C3 — mutual authentication. Authentication ran one way: the client proved itself, the node proved nothing. handshake_ack.node_pk was never verified against anything and per-chunk signatures had been dropped in Phase 9.15, so a peer that had hijacked signaling (C2) or been substituted by the hub could accept the client's proof, ignore it, and serve a forged index, forged chat history and a forged is_node_admin flag. The client now sends a nonce; the node answers with its own GEK proof over that nonce AND an Ed25519 signature over the transcript; the browser verifies both and refuses otherwise. It also refuses an unchallenged handshake_ack, which previously let a peer skip proving anything at all. L4 — the proof was nonce ‖ offer_fp ‖ answer_fp: bare concatenation, and a missing fingerprint silently degraded it to nonce-only, dropping MitM detection (NS5). Every field is now length-prefixed and domain-separated, the role is bound so a client proof cannot be replayed as a node proof, and an absent channel binding is refused rather than tolerated. M1 — group_id was optional; omitting it skipped the membership check entirely and fell back to the node's first group. Now mandatory. M9 — node-scoped daemon tokens are refused on the client path. NOT DONE: quic_server.py still runs its own JWT-only handshake, so C6 remains open — a forged or stolen token reaches a node over QUIC and can inject chat without holding the GEK. quic_binding() is written and unit-tested but unwired. 11.5.6 (whether the certificate-hash anchor works with aioquic, or an RFC 5705 exporter is reachable) is unproven. 11.5.8 TOFU pinning of pk_node is not done: the client verifies the node's signature but does not yet remember which key it saw last. Adds packages/meshbay-common/tests/test_handshake.py (18 tests) covering the properties every transport must inherit. WebRTC test helpers rewritten around the shared module; _make_jwt now defaults to the test group, since group_id is mandatory. Tests: 24 webrtc, 176+ node+common. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * fix: resource limits, signaling authz, node admin UI tokenChristophe Besson2026-08-137-32/+380
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5 — findings H6, C4 (partial), and milestone 11.5.3. H6 — resource exhaustion. Several paths let one peer degrade or stall a node: * the DataChannel frame limit was a flat 64 MB applied BEFORE authentication, so an unauthenticated peer could announce a huge frame and dribble bytes into it. Unauthenticated peers now get 64 KB; the large budget is granted only after the GEK proof, where it is needed for uploads. * _do_stream_segment ran subprocess.run(..., timeout=30) directly in the event loop, stalling the entire daemon — every peer, every group — for up to thirty seconds per request. Now async, with a timeout and process kill. * ffmpeg was spawned per stream request with no cap. Both streaming paths now share a transport-wide semaphore. * POST /v1/nodes/{id}/webrtc/offer was reachable by any authenticated user for any node, with no membership check and no rate limit, making the target node allocate an aiortc PeerConnection and gather ICE on demand — remote resource exhaustion against a third party's machine. Now rate limited, capped per user, SDP size bounded, and the caller must share an active group with the node. That also closes the H4 gap where signaling ignored group status. * POST /v1/nodes/{id}/incoming took peer_ip verbatim, so any user could make an arbitrary node emit UDP packets to an address of their choosing. The probe target must now match the caller's own source address. C4 (partial) — the pre-proof bundle window. GEK and keypair bundle fetches are served before the GEK proof by necessity: the client needs its wrapped bundle in order to compute the proof. That window is a disclosure surface a hub can reach by forging a JWT. Bounded to 4 fetches per session and audited as "pre_proof_fetch". The real fix is removing remote keypair bundles entirely, which belongs to the native client (Phase 13.3). 11.5.3 — the node admin UI was unauthenticated because it binds loopback. But any local process can reach it, and so can a page in the operator's browser via DNS rebinding — and this API re-initialises group keys and reads the audit log. H2 showed script execution there equals full control. Now gated by a per-run token, printed at startup, accepted as ?t= or X-MeshBay-Token. One test needed rewriting rather than adding: the first version asserted "subprocess.run(" was absent from the source, which also matched the comment documenting the old behaviour. It now parses the AST and checks the property. Tests: 121 node, 142 hub+common. Regression suite 47 node + 10 hub. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * fix: swarm privacy, revocation persistence, keystore KDF, audit integrityChristophe Besson2026-08-1314-66/+406
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5 hardening batch — H7, H4, M2, M6, M7, L1, L3, L6. H7 — private content hashes leaked to the hub. The daemon registered blake3 hashes for every group it hosted, private ones included, giving the hub a content fingerprint of every private file and letting anyone confirm whether a known file exists in the network. The leak was dormant only because the routes were declared on the groups router with a full path and mounted at /v1/groups/v1/swarm/* — the node's calls 404'd into a swallowed exception. Fixing the path alone would have activated the leak, so both land together: registration is gated on group visibility, the routes moved to a real /v1/swarm router, and the lookup now requires authentication. H4 — revocation was advisory. Group revocations were signed and broadcast by the hub and then dropped by the node, whose handler understood only "user" and "jti", so "suspend a group" enforced nothing. The denylist was also in-memory only, so a restart silently un-revoked everyone. Now persisted to data_dir/denylist.json, group targets honoured on both transports, and live sessions for a revoked group are closed. M2 — the node keystore, which protects the node's Ed25519 and X25519 private keys, was still deriving at 64 MB long after the hub's password verifier moved to 256 MB; the docs recorded the bump as done, true for the hub only. Raising the constant alone would have made every existing keystore permanently undecryptable, so envelopes now record the parameters they were written with and pre-M2 files continue to open under the legacy profile. M6 — registration inserted its audit row with a NULL user_id and then ran UPDATE ip_logs SET user_id=<new> WHERE user_id IS NULL, claiming every unattributed row in the table: failed logins for other usernames, concurrent registrations. In logs retained a year for legal requests, that attributed other people's connections to the wrong account. M7 — X-Forwarded-For was trusted unconditionally at four call sites, so anyone could forge the IP written to the compliance log and evade per-IP rate limits. New netutil.client_ip honours the header only from a trusted proxy and takes the rightmost hop (the one our proxy appended); no direct header reads remain. L1 dead GEK_REQUEST/GEK_RESPONSE constants removed; L3 peer errors no longer echo exception text (paths, internal state); L6 email sanity-checked instead of accepting any string — deliberately not RFC 5322, to avoid a new dependency. test_daemon_index_change_pushes_to_peers asserted that a PRIVATE group's hashes are registered with the hub. Split: private asserts not-called (index push to members still asserted), and a new test proves public groups still register. That is the fourth pre-existing test found asserting a vulnerability as intended behaviour, after gek auto-activation, the transport-wide chat_store and the blind admin challenge. Tests: 116 node, 132 hub+common. Regression suite now 43. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * fix(hub): authenticate node WebSocket registrationChristophe Besson2026-08-132-13/+256
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5 — finding C2 (see second-review.md). /v1/nodes/ws took node_id and group_ids straight from the client's first message with no ownership check: node_id = msg.get("node_id") or decoded.get("sub", "unknown") _connected_nodes[node_id] = ws Any registered user could connect with an ordinary browser token, claim a victim node's id and overwrite its entry. Every WebRTC offer for that node was then relayed to the attacker, who answered with their own SDP — full node impersonation. The DTLS channel binding does not help, because the attacker is the endpoint rather than a relay: the browser sends its GEK proof to the attacker, who ignores it and replies handshake_ack. The attacker received the victim's encrypted keypair bundle, chat and uploads, and could serve a forged index. Registration now requires scope == "node", verifies Node.user_id against the token subject, checks the account is active, and refuses to displace a live registration instead of silently overwriting it. group_ids are intersected with the operator's actual membership: a node may narrow the set to what it hosts but cannot widen it, so it cannot advertise itself as an online source for arbitrary groups. Authorization uses a short-lived session rather than Depends(get_db): a node WebSocket lives for hours and a request-scoped dependency would pin a PostgreSQL connection for its whole lifetime. BEHAVIOUR: a node hosting a group whose hub membership was never recorded for the operator's account will stop appearing in GET /v1/groups/{id}/nodes. Adds tests/test_node_ws_auth.py (7 tests). The node WebSocket had no test coverage at all, which is why this went unnoticed. Tests: 109 node, 139 hub+common. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * fix(node): group isolation, upload confinement, GEK seizure, admin challengeChristophe Besson2026-08-1311-155/+919
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5 — findings H1, C5a, H2, C5b, H5 (see second-review.md). Batched together because the node-side changes share webrtc_server.py and cannot be separated into working commits. H1 — cross-group chat leak. chat_store, the peer registry and the display-name cache were read from the shared transport context, and daemon.py hoisted the FIRST group's chat store onto it. On a node hosting several groups every group's messages went to one database, chat_history served them back to members of every other group, and chat broadcast reached all peers regardless of group. All three now resolve through _group_ctx(). C5a — upload confinement. Uploads landed in the shared root under a client-chosen name and overwrote whatever was there. Any member could destroy the operator's files, and by becoming the recorded uploader of the replaced file could then delete it through the uploader path, bypassing the Ed25519 admin challenge. Uploads now go to a per-user quarantine (.uploads/{user_id}/), refuse to overwrite, and enforce chunk ordering, a filename allowlist and a size cap. H2 — stored XSS in the node admin UI. Filenames chosen by any group member were interpolated raw into the localhost UI, which has no authentication, so script execution there equals control of the node admin API. Now html.escape() throughout, textContent in the audit table, plus CSP/nosniff/no-referrer. The CSP contains exfiltration but cannot stop injected inline script — escaping is the fix. C5b — group key seizure. gek_bundle_store wrote whatever any member sent and auto-activated bundles addressed to the node operator. The operator's X25519 public key is public (the node publishes it in handshake_ack), so any member could wrap a key of their choosing for it and take over the group, locking every legitimate member out. Storing now requires an operator signature and _try_activate_gek is removed: nothing arriving over MNP can set a live GEK. H5 — unbound signing oracle. The node challenged with 32 raw random bytes and the client signed them blind, so a signature named no operation, subject, node or time. New meshbay_common/adminop.py defines a length-prefixed, domain-separated transcript; both sides build it independently and the client refuses to sign when the announced op/subject do not match its request. BREAKING: a group admin who does not operate the node can no longer store GEK bundles on it. Invites must be performed by the node operator. Adds tests/test_security_regressions.py. Verified against pre-fix source via git stash. Three pre-existing tests asserted the vulnerable behaviour as a feature and were inverted: gek auto-activation, and the transport-wide chat_store in test_daemon. Tests: 109 node, 132 hub+common. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * fix(node)!: remove unauthenticated HTTP file API and TCP transportChristophe Besson2026-08-1312-1346/+45
|/ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 11.5.A — findings C1 and C6 (see second-review.md). C1: the per-group HTTP file API bound 0.0.0.0 for every configured group, private ones included, and served two endpoints with no authentication at all: GET /index (full Mesh Group Index) and GET /file/{id} (raw plaintext file via FileResponse). Anyone able to reach the port — LAN, forwarded port, permissive IPv6 — read every private file. This bypassed the entire GEK-proof and node sovereignty layer. Deleted rather than patched: it duplicated MNP without any of its controls. C6: the TCP+TLS chunk server accepted a bare JWT with no GEK proof, leaving a second non-compliant handshake path. Deleted; QUIC remains and will be brought to parity with WebRTC by the unified handshake in 11.5.4. Transport decision recorded in transport/__init__.py: WebRTC/ICE is primary for browser and native clients (the only NAT traversal validated here — 2 ISPs, IPv4 STUN + IPv6, 4G CGNAT); QUIC is kept for LAN, port-forwarded and hub-less group:// access. punch_nat() is a direct-connection helper, not a traversal stack. Also removed server_ssl_context()/client_ssl_context() from tls_cert.py (no remaining callers) and a dead import of the former in quic_server.py. generate_self_signed_cert() stays: QUIC uses it, and the certificate hash is the intended channel-binding anchor for 11.5.6, since QUIC has no DTLS fingerprint to bind the GEK proof to. BREAKING CHANGE: node.toml keys `port` and `http_port` are gone. Regenerate config with `meshbay-node init`. Env var MESHBAY_PORT -> MESHBAY_QUIC_PORT. Tests: 198 passed (209 - 7 test_http_server - 4 test_transport). No other test changed status. Net -1300 lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: second security review + roadmap rewrite0.1Christophe Besson2026-08-134-59/+1451
| | | | | | | | | | | | | | | | | | 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>
* feat: Phase 12 — P2P crypto material, password split, node Ed25519 authChristophe Besson2026-08-1330-787/+3584
| | | | | | | | | | | | | | | | 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>
* feat(node): audit logging + local admin UI rewriteChristophe Besson2026-08-115-133/+705
| | | | | | | | | | | | | | | | | | | Add SQLite audit store for legal compliance (LCEN/DSA): logs user IP, actions (handshake, file download/upload/delete, stream, chat), and timestamps. Retention: 1 year, with cleanup method. WebRTC transport now logs all user actions to the audit store with remote IP extraction from the ICE transport. Local web UI rewritten as a proper admin dashboard: - Stats cards (groups, files, peers) - Connected peers table with IP, username, group, state - Group cards with file listings and shared directory info - Audit log page with event/user filtering - Dark theme, responsive, auto-refresh - JSON API: /api/status, /api/groups, /api/peers, /api/audit, /api/config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(node): index push to WebRTC peers + swarm registration (11.5, 11.9)Christophe Besson2026-08-114-18/+189
| | | | | | | | | | | 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>
* feat(node): Phase 11 — production-ready daemon with WebRTC, WS, chat, HTTPChristophe Besson2026-08-117-65/+454
| | | | | | | | | | | | | | | | | | 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>
* fix(ui): reduce upload chunk size to 48KB to fit aiortc SCTP limitChristophe Besson2026-08-111-1/+1
| | | | | | | | | | aiortc advertises maxMessageSize=65536 in SDP. A 64KB data chunk + msgpack envelope + 4-byte length prefix exceeds this limit, causing "Trying to send message larger than max-message-size" on upload. 48KB data + overhead stays well under 65536 bytes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): set MediaSource duration from ffprobe and clamp seeks to buffered rangeChristophe Besson2026-08-111-5/+23
| | | | | | | | | | | | | | | Two fixes for the MSE video player: 1. Set mediaSource.duration from the ffprobe-reported duration on sourceopen, so the seek bar shows the correct video length instead of NaN/infinite. 2. Use SourceBuffer mode='sequence' for sequential append without timestamp gaps. Add a seeking handler that clamps currentTime to the buffered range — seeking beyond buffered data snaps back instead of freezing the video. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): remove duplicate connecting messages and buggy stream progress barChristophe Besson2026-08-112-52/+4
| | | | | | | | | | | | | | 1. "Connecting..." was shown in 3 places simultaneously (status badge, cached files area, and general status). Now only the badge shows it when cached files are visible — the redundant messages are removed. 2. The floating stream progress bar caused constant re-renders during video streaming (every 256KB segment triggered setState). The fMP4 remux size also differs from the original file size, making the progress inaccurate. Removed the overlay bar entirely — the video element's native buffered range indicator is sufficient. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Phases 11–17 roadmap (node daemon, CLI, Sender Keys, Android, ↵Christophe Besson2026-08-111-31/+155
| | | | | | | | | | 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>
* feat: Phase 10c — MSE video streaming (real-time playback)Christophe Besson2026-08-119-54/+467
| | | | | | | | | 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>
* fix(hub): move _handle_chat_notify before @router.websocket decoratorChristophe Besson2026-08-111-1/+1
| | | | | | | The function was placed between the decorator and node_websocket, breaking the WebSocket endpoint registration (403 on all WS connects). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: chat notifications via hub, file delete, upload fix, cached display, ↵Christophe Besson2026-08-112-0/+48
| | | | | | | | | | | | | | | | | | | | | inline thumbnails Backend: - Chat store persists sender_name (SQLite migration, no more UUID display) - FILE_DELETE / FILE_DELETE_ACK MNP types — node admin can delete files - Node sends chat_notify to hub WS — hub creates notifications for offline members - Hub revocation.py handles chat_notify, creates per-member notifications Frontend: - Upload chunk size 64KB (was 1MB) — fixes WebRTC DataChannel max-message-size - Show cached files immediately while WebRTC connects - ChatImage component — inline image thumbnails in chat (download+decrypt) - File delete action in menu (group admin, with confirm dialog) - Member panel: "Owner" label instead of confusing "Group admin" - Create group page: hint about needing a node - Refresh index after chat attachment upload Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): upload chunk size, cached file display, chat names, file delete, ↵Christophe Besson2026-08-117-26/+179
| | | | | | | | | | | | | | | | inline thumbnails - Upload chunks capped at 64KB to avoid WebRTC DataChannel max-message-size - Show cached files immediately while WebRTC connects (tabs visible during connection) - Persist sender_name in chat store (SQLite) — no more UUID display in history - File delete action in menu (node admin only, enforced server-side) - FILE_DELETE / FILE_DELETE_ACK MNP message types - Inline image thumbnails in chat attachments (download+decrypt, Signal-style) - Member panel: "Owner" label instead of "Group admin" to avoid hub/group admin confusion - Create group page: hint about needing a node - Refresh index after chat file attachment upload Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(node): iterate dict copies in WebRTC peer broadcast and close_allChristophe Besson2026-08-111-2/+2
| | | | | | | Concurrent peer disconnects could mutate _peers/_sessions during iteration, causing RuntimeError: dictionary changed size during iteration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): group creation error, chat UUIDs, file preview, action menuChristophe Besson2026-08-115-53/+428
| | | | | | | | | | | | | | | | | | | Bug fixes: - Group creation "[object Object]" error: removed dead pkcs8 import code that threw before GEK wrapping, added array detail handling in hubFetch - Chat shows usernames instead of UUIDs (sender_name passed through node) - Join button: navigate to group on "Already a member" instead of error UI improvements: - Loading spinner animation for async states (connecting, fetching) - File action menu (3-dot dropdown: View, Download, Play) - Click filename to preview inline (images, text/code files) - FilePreview overlay for images and text files - Chat file attachment button (upload to node + structured message) - Chat attachment display (icon, filename, size) - Member panel: "Group admin" badge instead of plain "Admin" text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: Phase 10b — Self-service UI (group create/join, upload, IndexedDB, ↵Christophe Besson2026-08-1113-15/+974
| | | | | | | | | | | | | | | | 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>
* feat(hub): Phase 10.5–10.8, 10.10 — notifications, settings, search, versionChristophe Besson2026-08-1113-29/+543
| | | | | | | | | | | | - 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>
* docs: update Phase 10 commit hash, API reference, key modulesChristophe Besson2026-08-113-6/+25
| | | | Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(hub): Phase 10.1–10.4 — Site overlay + admin/moderation UIChristophe Besson2026-08-1115-34/+1646
| | | | | | | | | | | | | | - 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>
* docs: update Phase 9 commit hash in devel-phases-nextChristophe Besson2026-08-111-1/+1
| | | | Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: Phase 9 — Web client SPA with WebRTC P2P transportChristophe Besson2026-08-1127-316/+3039
| | | | | | | | | | | | | | | | 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>
* feat: Phase 9.1–9.5 — WebRTC DataChannel transport for browser P2PChristophe Besson2026-08-1015-172/+3045
| | | | | | | | | | | | | | | | | | | | | | | | 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>
* feat(hub): Phase 8 — Hub v2 security hardening + production readinessChristophe Besson2026-08-1020-90/+508
| | | | | | | | | | | | | | | | | | | | 8.1 Config-based admin authz (require_admin on all admin endpoints) 8.2 Email encrypted at rest (AES-256-GCM, HKDF from hub Ed25519 key) 8.3 Refresh token rotation with family-based reuse detection 8.4 Federation persistence (HubPeer model replaces in-memory dict) 8.5 Federation token verification now async (DB-backed) 8.6 CSAM hash check wired into swarm registration flow 8.7 Rate limiting on auth endpoints (5/10/20 per minute) 8.8 Healthcheck endpoint (GET /v1/health, no auth) 8.9 IP log cleanup background task (365-day retention) 8.10 Argon2id params bumped to 256 MB (pw_version, rehash on login) Deployed to meshbay.org — schema migrated, existing emails encrypted. 117 tests pass (29 hub, 88 common+node). Resolves security review items S1, S2, S5. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: Phase 7 — Node v2 (multi-group, Sender Keys, 0-RTT, chat, denylist)Christophe Besson2026-08-1026-155/+2151
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
* docs: update draft v3 + phases-next with Phase 7 decisionsChristophe Besson2026-08-102-24/+87
| | | | | | | | | | 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>
* docs: add devel-phases-next.md — Phases 7-12 roadmapChristophe Besson2026-08-101-0/+149
| | | | | | | | | | | | | 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>
* docs: add demo-v2 NAT findings to CLAUDE.mdChristophe Besson2026-08-101-0/+14
| | | | | | Port-Restricted Cone SFR, punch_nat() mechanism, 12.7s handshake note. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* docs: update devel-phases — demo-v2 NAT traversal QUIC validatedChristophe Besson2026-08-101-0/+4
| | | | | | | SFR Port-Restricted Cone NAT diagnosed. punch_nat() implemented. QUIC direct Fedora→SFR→meshbay.org: 12.7s handshake, file transfer OK. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(node): punch_nat() — NAT traversal from QUIC server socketChristophe Besson2026-08-101-0/+16
| | | | | | | | | | | | | | | | | | | SFR residential NAT is Port-Restricted Cone: inbound is only allowed from (peer_ip, peer_port) if the node previously sent a packet TO (peer_ip, peer_port) from the SAME socket. punch_nat(peer_ip, peer_port): sends a probe UDP packet from the QUIC server's own transport (_transport.sendto), creating the correct NAT entry. Used after server.start() to enable direct QUIC connections through SFR NAT without UPnP or relay. demo-v2 result: QUIC/UDP direct Fedora→SFR NAT→meshbay.org validated. Connection time 12.7s (QUIC handshake through NAT). File transfer 700B. QuicChunkClient local_port param: ensures client binds to same port as punch_nat destination (Port-Restricted Cone requirement). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(node): QuicChunkClient local_port + QuicChunkServer dual-stackChristophe Besson2026-08-102-8/+11
| | | | | | | | local_port=0 param on QuicChunkClient — specify for Port-Restricted Cone NAT hole punching (client must send from the same port the node probed to). QuicChunkServer default host '::' for IPv4+IPv6 dual-stack on Linux. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix: 4 corrections — streaming hash, watchdog bug, cipher doc, depsChristophe Besson2026-08-092-4/+23
| | | | | | | | | | | | | | | | | | | 1. indexer.py: streaming blake3 (8MB chunks) instead of read_bytes(). Large files (initrd.img, ISOs, VM images) no longer load into RAM. 2. QE/demo-v1/run_node.py: call indexer.start() not initial_scan(). initial_scan() alone never starts the watchdog observer — files added after startup were silently ignored. Added indexer.stop() on shutdown. 3. USERGUIDE.md §8: clarify symmetric vs asymmetric. Ed25519/X25519 = asymmetric (key pairs). ChaCha20-Poly1305 and AES-256-GCM = symmetric AEAD 256-bit (content encryption). ChaCha20 is PRIMARY; AES-GCM is optional browser-compat variant only. 4. pyproject.toml: aioquic, websockets, aiosqlite, slowapi added to proper package deps (were installed manually, now declared). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix: complete pyproject.toml deps + graceful QUIC fallbackChristophe Besson2026-08-095-21/+57
| | | | | | | | | | | | | | | | | | | | meshbay-node/pyproject.toml: add aioquic>=1.0 (was commented 'v2'), websockets>=12.0 (revocation push). Both are production code since Phase 5. meshbay-hub/pyproject.toml: add aiosqlite (tests without PostgreSQL), slowapi (rate limiting), websockets (revocation push), PyJWT (explicit). transport/__init__.py: QUIC imports wrapped in try/except — node works without aioquic (TCP+TLS + HTTP fallback). QUIC_AVAILABLE flag exported. QUICKSTART.md: replace manual pip list with 'pip install -e' that pulls all deps from pyproject.toml automatically. Add dependency table. CLAUDE.md: clarify that all deps go in pyproject.toml, not manual installs. 81/81 tests. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix: venv --clear required when copying repo across OS (Fedora→Ubuntu)Christophe Besson2026-08-092-15/+11
| | | | | | | | | | | | | Root cause: certifi.where() in the Fedora venv points to /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem which does not exist on Ubuntu. 'python3 -m venv .venv' without --clear keeps the Fedora certifi paths. Fix: always use --clear when recreating a venv on a different OS. Documented in QUICKSTART.md and CLAUDE.md. rsync command updated to exclude .venv/ (in QE/server-state, not versioned). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix: document pip SSL_CERT_FILE workaround for Python 3.14 on Ubuntu/FedoraChristophe Besson2026-08-092-14/+29
| | | | | | | | pip + Python 3.14 fails with FileNotFoundError in certifi.where() on fresh venvs (truststore bug). Fix: SSL_CERT_FILE pointing to system CA bundle. Documented in CLAUDE.md (Python environment section) and QUICKSTART.md. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* docs: update all pointers after keyderive + QE restructureChristophe Besson2026-08-095-17/+183
| | | | | | | | | | | | | | | | | | | | | | CLAUDE.md: add QE/ to structure, key modules table, server state reference, security rule updated (QE/ not keypair files), meshbay.org inventory pointer. devel-phases.md: add milestones 6.6-6.9 (keyderive, bundle, demo scripts, QUICKSTART rewrite). 81/81 tests. docs/meshbay-draft-v3.md §6.1.1: new section documenting 3 key generation strategies (Argon2id CLI, WebCrypto browser+bundle, keystore file) and the algorithm mismatch caveat between CLI and web registration paths. docs/USERGUIDE.md §2 Register+Login: replace "generate and persist before registering" warning with the two clean strategies (derive_keys_from_password for CLI, keyderive.js + keypair_bundle for browser). Login response updated with keypair_bundle field. hub/models.py + users.py + Alembic migration: keypair_bundle column on User, stored at registration, returned at login (web clients only). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>