summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/db
Commit message (Collapse)AuthorAgeFilesLines
* fix: stop a stream on close, count only real users, record where a node isChristophe Besson2026-08-152-0/+42
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | **Closing the viewer left the node working.** Nothing told it to stop: the player dropped its handlers, which only made the browser deaf. ffmpeg kept running and held one of the node's two transcode slots until the credit timeout expired two minutes later — which is why the next video answered "server busy". `stream_stop` ends it at once, and the viewer also drops its queue, ends the MediaSource and revokes the object URL on the way out, any of which could be holding megabytes of decrypted video. While there: `file_chunk` replies were matched to their requests by arrival order, which was true by luck rather than by construction. The reply now names the file it belongs to and is matched on that and the chunk index; a chunk nobody is waiting for is dropped instead of being handed to whatever request happens to be oldest. **The administration panel counted its own history.** A deleted account is tombstoned so the connection log stays readable, and every count and list treated that row as a user — including a group's member count, and the member list of the group itself. They do not any more. **Where a node is.** `endpoint_hint` is what a node believes its address to be, learned from a STUN server and sent to us: useful for reaching it, and a claim. The announcement that carries it is signed with the node key over a fresh timestamp, so the address that request *arrives from* is the address of whoever holds that key — that is now recorded on the node row and shown in a Nodes tab, next to the hint, with the difference spelled out. Clients get the same treatment: `webrtc_offer` is logged with the address the hub saw when a browser starts a peer connection. Verified against the live deployment: the node's row reads 90.112.206.172 after a restart, and in e2e a stopped stream goes quiet in one message and the next one starts immediately instead of being refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(logs): keep the username on records the account no longer answers forChristophe Besson2026-08-152-0/+36
| | | | | | | | | | | | | | | | | | | | | The connection log took the name from a join on `users`, and deletion tombstones that row — so every record belonging to a deleted account reported `deleted-3f9a1c`, which is the one answer that helps nobody. The log is kept for a legal retention period precisely so it can say who did what; losing the name at deletion kept the data and lost the point of it. `ip_logs.username` is written as the account is erased, and stays NULL while the account is alive, where the join is better because it cannot go stale. The admin view prefers the stored name when there is one: the join still answers after deletion, just with the tombstone. Releasing the username for re-registration and keeping it in the log are separate things, and the guide now says so. On the node side, the pre-proof audit line records the username the session already knew, instead of leaving the column empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Notifications: one per conversation, none for your own messagesChristophe Besson2026-08-142-0/+49
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Four things were wrong, and they compounded: a busy chat produced one row per message, muting a group did nothing at all, there was no way to clear the list, and the one person guaranteed to know about a message — its author — was told about it. The author bug was a name mismatch across two processes. The node sent chat_notify without saying who wrote the message, so the hub used the node's own token subject, which is the operator's account. The skip therefore matched the operator and no one else: everybody was notified of their own messages, and the operator was notified of nobody's. The node now names the author and the hub reads that field. Muting lived in the browser's localStorage and nothing ever read it, so the checkbox was decoration. It is a column on group_members now, checked where the notification is created — a notification nobody wants is not written at all. Chat keeps a single row per (user, kind, group) whose date moves and whose read flag clears, so a conversation is one line saying when it last spoke. Clicking it opens the group and dismisses it; joining a group dismisses its invitation; and DELETE /v1/notifications clears the lot. The hub deploy now runs alembic. create_all() only creates missing tables, so group_members.muted never arrived on the running hub and /v1/groups/mine answered 500 — worth catching in the script rather than in a browser. Verified end to end against the deployed hub and node: the author receives nothing, the other member receives exactly one, carrying its group_id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat!: identity keys per node — C4's blast radius drops to one operatorChristophe Besson2026-08-142-2/+41
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
* feat: Phase 12 — P2P crypto material, password split, node Ed25519 authChristophe Besson2026-08-134-38/+5
| | | | | | | | | | | | | | | | 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(hub): Phase 10.5–10.8, 10.10 — notifications, settings, search, versionChristophe Besson2026-08-111-0/+20
| | | | | | | | | | | | - 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>
* feat(hub): Phase 10.1–10.4 — Site overlay + admin/moderation UIChristophe Besson2026-08-111-0/+1
| | | | | | | | | | | | | | - 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>
* feat(hub): Phase 8 — Hub v2 security hardening + production readinessChristophe Besson2026-08-101-1/+16
| | | | | | | | | | | | | | | | | | | | 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>
* docs: update all pointers after keyderive + QE restructureChristophe Besson2026-08-091-0/+83
| | | | | | | | | | | | | | | | | | | | | | 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>
* feat: password-based key derivation + operational QUICKSTARTChristophe Besson2026-08-091-2/+3
| | | | | | | | | | | | | | | | | | | | | keyderive.py: derive Ed25519+X25519 from username+password via Argon2id. Same credentials → same keys on any device. Encrypt/decrypt keypair bundle (AES-256-GCM) for hub storage (web clients). 7/7 tests. Full suite: 81/81. keyderive.js: browser counterpart using PBKDF2-SHA512 + random keypairs encrypted for hub storage. Avoids algorithm mismatch with Python. hub/models.py + users.py: keypair_bundle field added to User, stored on registration, returned in login response for web client key recovery. QUICKSTART.md: fully rewritten. 3 operational scripts in QE/demo-v1/: setup_demo.py — create accounts, group, distribute GEK run_node.py — start HTTP node (watches shared/ directory) download.py — bob login → GEK fetch → decrypt → save All tested locally end-to-end. No invented URLs. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat: Phase 6 complete — chat, multi-group, federation, replication, webcryptoChristophe Besson2026-08-091-0/+36
| | | | | | | | | | | | | | | | | | | | | | | | | | 6.1 Double Ratchet (meshbay_common/ratchet.py): Forward secrecy, break-in recovery, out-of-order delivery. Signal-spec KDF_RK/KDF_CK via HKDF-SHA256. 11/11 tests. 6.2 Multi-group node (config.py): [[groups]] TOML array, per-group ports, back-compat [group]. 6.3 MHP federation persistence (db/models.py FederatedGroup + SwarmSource): receive_directory() now persists to federated_groups table. list_public_groups() includes federated results with source attribution. 6.4 Content replication (node/replication.py + hub SwarmSource): ContentReplicator: fetch-index, download, hash-verify, register-swarm. Hub: POST /v1/swarm/register, GET /v1/swarm/{hash} for multi-source. 6.5 Browser private group (webcrypto.py + static/crypto.js): AES-256-GCM variant of GEK for WebCrypto-compatible groups. crypto.js: SubtleCrypto importGEK + deriveChunkKey + decryptChunk. Keys distinct from ChaCha20 via :aes HKDF info suffix. 4/4 tests. 74/74 tests total. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(hub): add moderation — content blocklist + reports — 5.9Christophe Besson2026-08-091-0/+35
| | | | | | | | | | | DB: ContentReport + ContentBlocklist tables. POST /v1/reports: public endpoint, auto-blocks after 2 reports. GET /v1/blocklist/check: node sync check before serving public content. GET /v1/blocklist: full list for node startup sync. GET|POST|DELETE /v1/admin/blocklist: admin management. 6/6 tests. Full suite: 59/59. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(hub): add SQLAlchemy 2.0 async DB layer + AlembicChristophe Besson2026-08-097-0/+475
| | | | | | | | Models: User, Node, Group, GroupMember, GEKBundle, RefreshToken, IPLog. Engine configurable via MESHBAY_DATABASE_URL (asyncpg/aiosqlite). Alembic async env.py + initial_schema migration autogenerated. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* chore: initialize monorepo structure for MeshBayChristophe Besson2026-08-091-0/+0
3-package layout: meshbay-common (shared crypto/protocol), meshbay-hub (FastAPI server), meshbay-node (local daemon). Includes validated POC spikes 1-6 in poc/, architecture drafts v1/v2 in docs/, and CLAUDE.md project conventions. All cryptographic primitives extracted from POC into meshbay_common/crypto.py (GEK wrap/unwrap, chunk key derivation, keystore encryption, chunk signing). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>