diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-28 21:35:20 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-28 21:35:20 +0200 |
| commit | 91505face56f7ee6817408e52bad7902add75f09 (patch) | |
| tree | 9902490872f441c5c88ea1f7ab57288ab6507899 /packages | |
| parent | a421a03d2be16670dc8d9076d26f4a7eac669986 (diff) | |
| download | meshbay-91505face56f7ee6817408e52bad7902add75f09.tar.gz | |
refactor: remove the public-content swarm
Nodes registered the hashes of their public groups on the hub and nothing
ever read them back. Routes, model and node registration removed; a
migration drops swarm_sources. No node sends the hub a content hash now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
11 files changed, 57 insertions, 329 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py index 52d9f60..df2f336 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py @@ -20,16 +20,11 @@ from meshbay_hub.db.models import ( Group, GroupMember, IPLog, - SwarmSource, User, ) router = APIRouter(prefix="/v1/groups", tags=["groups"]) -# Swarm endpoints live at /v1/swarm/*. They were previously declared on the groups -# router with a full path, which mounted them at /v1/groups/v1/swarm/* (H7). -swarm_router = APIRouter(prefix="/v1/swarm", tags=["swarm"]) - @router.get("/mine") async def my_groups( @@ -211,109 +206,6 @@ async def list_public_groups( return {"groups": groups, "total": len(groups)} -# ── Swarm (content replication) ─────────────────────────────────────────────── - -class SwarmRegisterRequest(BaseModel): - content_hash: str # blake3 hex - endpoint: str # "<scheme>:<port>" — a port on the caller, never a host - - -# A transport and a port, and deliberately no host. The field used to be free -# text documented as "ip:port", so a caller could name *someone else's* -# address as a source; nothing dials a swarm source today, which is the only -# reason that was not already a reflection primitive. A reader learns where a -# node is from the node record, which is stamped with the address the announce -# actually came from — so a host here would be a second, weaker, answer to a -# question already settled elsewhere. -_SWARM_ENDPOINT = re.compile(r"^(webrtc|quic):([0-9]{1,5})$") - -# One account, this many public hashes. Rows are keyed (hash, account) with no -# cap, so a loop of invented hashes was unbounded storage growth on a hub -# shared with everyone else. A public library far larger than this is a real -# thing — but it is one a hub operator should be asked about, not something a -# client establishes by writing rows. -MAX_SWARM_HASHES_PER_ACCOUNT = 10_000 - - -@swarm_router.post("/register", status_code=201) -@limiter.limit("120/minute") -async def swarm_register( - body: SwarmRegisterRequest, - request: Request, - current_user: User = Depends(get_current_user), - db: AsyncSession = Depends(get_db), -): - """ - Node registers itself as a source for a PUBLIC content hash. - - Finding H7: the node registered hashes for every group it hosted, private ones - included, and this route was mounted at /v1/groups/v1/swarm/register — so the - node's calls 404'd and the leak was masked by a routing bug rather than - prevented. Nodes now filter by group visibility before calling, and the path is - correct, so the filter has to be right. - - Availability: the endpoint is a port, not an address, and the number of - hashes one account may claim is bounded. See the two constants above. - """ - m = _SWARM_ENDPOINT.match(body.endpoint or "") - if not m or not (0 < int(m.group(2)) < 65536): - raise HTTPException( - status_code=422, - detail="endpoint must be '<webrtc|quic>:<port>' — a port on the " - "registering node, not an address") - - from datetime import datetime - existing = await db.get(SwarmSource, (body.content_hash, current_user.id)) - now = datetime.now(UTC) - if existing: - existing.endpoint = body.endpoint - existing.last_seen = now - else: - held = (await db.execute( - select(func.count()).select_from(SwarmSource) - .where(SwarmSource.node_id == current_user.id))).scalar() or 0 - if held >= MAX_SWARM_HASHES_PER_ACCOUNT: - raise HTTPException( - status_code=429, - detail="This account already claims the maximum number of " - "public content hashes") - db.add(SwarmSource( - content_hash=body.content_hash, - node_id=current_user.id, - endpoint=body.endpoint, - )) - await db.commit() - return {"status": "registered", "hash": body.content_hash} - - -@swarm_router.get("/{content_hash}") -async def swarm_sources( - content_hash: str, - current_user: User = Depends(get_current_user), - db: AsyncSession = Depends(get_db), -): - """ - Return nodes that can serve a content hash. - - Authenticated (H7): an open endpoint lets anyone probe whether a given file - exists anywhere in the network and which node holds it. - """ - from datetime import datetime, timedelta - cutoff = datetime.now(UTC) - timedelta(minutes=30) - result = await db.execute( - select(SwarmSource) - .where( - SwarmSource.content_hash == content_hash, - SwarmSource.last_seen > cutoff, - ) - ) - sources = result.scalars().all() - return { - "hash": content_hash, - "sources": [{"node_id": s.node_id, "endpoint": s.endpoint} for s in sources], - } - - @router.get("/{group_id}/members") async def group_members( group_id: str, diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index 7046c2f..3e996a7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -41,7 +41,6 @@ from meshbay_hub.db.models import ( Node, Notification, RefreshToken, - SwarmSource, User, UserDevice, UserPreference, @@ -1379,14 +1378,13 @@ async def erase_account(db: AsyncSession, user: User, owned_groups: str = "refus the person it is about. Gone: credentials, email, node key, group memberships, notifications, refresh - tokens, node registrations, device keys, public-swarm sources. The username + tokens, node registrations, device keys. The username is released. Device keys go even though the desktop client keeps its private half: left behind, the key still belongs to this tombstone, so an account created later from the same installation is refused that device ("belongs to another - account"). Swarm sources are keyed by the *user* id and carry the node's - ip:port. + account"). Kept: the row itself, emptied, and the IP log that points at it. Those logs exist for one year to answer legal requests, and a log that cannot say whose @@ -1419,7 +1417,6 @@ async def erase_account(db: AsyncSession, user: User, owned_groups: str = "refus await db.execute(delete(RefreshToken).where(RefreshToken.user_id == user.id)) await db.execute(delete(Node).where(Node.user_id == user.id)) await db.execute(delete(UserDevice).where(UserDevice.user_id == user.id)) - await db.execute(delete(SwarmSource).where(SwarmSource.node_id == user.id)) await db.execute(delete(EmailVerification).where(EmailVerification.user_id == user.id)) # Links this account issued for a group it no longer owns; the ones for its # own groups went with them above. A used link keeps pointing at the diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py index cca1e6b..29de118 100644 --- a/packages/meshbay-hub/src/meshbay_hub/app.py +++ b/packages/meshbay-hub/src/meshbay_hub/app.py @@ -21,7 +21,6 @@ from meshbay_hub.api.admin import router as admin_router from meshbay_hub.api.deps import set_admin_usernames from meshbay_hub.api.federation import router as federation_router from meshbay_hub.api.groups import router as groups_router -from meshbay_hub.api.groups import swarm_router from meshbay_hub.api.health import router as health_router from meshbay_hub.api.hub import router as hub_router from meshbay_hub.api.hub import set_config as hub_set_config @@ -199,7 +198,6 @@ def create_app(cfg: HubConfig | None = None) -> FastAPI: app.include_router(groups_router) app.include_router(invite_links_router) app.include_router(invite_links_redeem_router) - app.include_router(swarm_router) app.include_router(revocation_router) app.include_router(moderation_router) app.include_router(federation_router) diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e6f7a8b9c0d1_drop_swarm_sources.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e6f7a8b9c0d1_drop_swarm_sources.py new file mode 100644 index 0000000..0e61390 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e6f7a8b9c0d1_drop_swarm_sources.py @@ -0,0 +1,38 @@ +"""drop the public-content swarm table + +Nodes registered the hashes of their public groups here and nothing ever read +them back: the swarm was written and never used. + +Revision ID: e6f7a8b9c0d1 +Revises: c4d5e6f7a8b9 +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "e6f7a8b9c0d1" +down_revision: str | Sequence[str] | None = "c4d5e6f7a8b9" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.drop_index("ix_swarm_hash", table_name="swarm_sources") + op.drop_table("swarm_sources") + + +def downgrade() -> None: + op.create_table( + "swarm_sources", + sa.Column("content_hash", sa.String(64), nullable=False), + sa.Column("node_id", sa.String(36), nullable=False), + sa.Column("endpoint", sa.String(128), nullable=False), + sa.Column("registered_at", sa.DateTime(timezone=True), nullable=False, + server_default=sa.func.now()), + sa.Column("last_seen", sa.DateTime(timezone=True), nullable=False, + server_default=sa.func.now()), + sa.PrimaryKeyConstraint("content_hash", "node_id"), + ) + op.create_index("ix_swarm_hash", "swarm_sources", ["content_hash"]) diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py index 09eb437..288f1e7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/models.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py @@ -266,22 +266,6 @@ class UserDevice(Base): __table_args__ = (Index("ix_user_devices_user", "user_id"),) -class SwarmSource(Base): - """ - Tracks which nodes can serve a given content hash (public swarm). - Hub maintains this for load-balanced public content delivery. - """ - __tablename__ = "swarm_sources" - - content_hash: Mapped[str] = mapped_column(String(64), primary_key=True) - node_id: Mapped[str] = mapped_column(String(36), primary_key=True) - endpoint: Mapped[str] = mapped_column(String(128), nullable=False) - registered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) - last_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) - - __table_args__ = (Index("ix_swarm_hash", "content_hash"),) - - class Notification(Base): __tablename__ = "notifications" diff --git a/packages/meshbay-hub/tests/test_account_deletion.py b/packages/meshbay-hub/tests/test_account_deletion.py index 2653f0d..64c2be8 100644 --- a/packages/meshbay-hub/tests/test_account_deletion.py +++ b/packages/meshbay-hub/tests/test_account_deletion.py @@ -130,16 +130,9 @@ def _device_pk() -> str: @pytest.mark.asyncio -async def test_deletion_clears_device_keys_and_swarm_sources(client, db_session): - """ - The privacy statement says every account row goes but the IP log. Swarm - sources are keyed by the *user* id despite the column's name, and carry the - node's transport and port — `webrtc:<port>`, which is what `daemon.py` - actually sends. This asked with `192.0.2.7:4433`, from the days when the - field was free text documented as "ip:port": a shape no node has ever - produced, and one that let a caller name a third party's address. - """ - from meshbay_hub.db.models import SwarmSource, UserDevice +async def test_deletion_clears_device_keys(client, db_session): + """The privacy statement says every account row goes but the IP log.""" + from meshbay_hub.db.models import UserDevice token, password = await _register(client, "devicer_test") headers = {"Authorization": f"Bearer {token}"} @@ -149,15 +142,10 @@ async def test_deletion_clears_device_keys_and_swarm_sources(client, db_session) r = await client.post("/v1/users/devices", headers=headers, json={"pk_auth_ed25519": _device_pk(), "label": "desktop"}) assert r.status_code == 201, r.text - r = await client.post("/v1/swarm/register", headers=headers, - json={"content_hash": "ab" * 32, "endpoint": "webrtc:4433"}) - assert r.status_code == 201, r.text # Present before, or the emptiness asserted below proves nothing. assert (await db_session.execute( select(UserDevice).where(UserDevice.user_id == uid))).scalars().all() - assert (await db_session.execute( - select(SwarmSource).where(SwarmSource.node_id == uid))).scalars().all() r = await client.request("DELETE", "/v1/users/me", headers=headers, json={"auth_key": _auth_key(password, "devicer_test")}) @@ -166,8 +154,6 @@ async def test_deletion_clears_device_keys_and_swarm_sources(client, db_session) db_session.expire_all() assert (await db_session.execute( select(UserDevice).where(UserDevice.user_id == uid))).scalars().all() == [] - assert (await db_session.execute( - select(SwarmSource).where(SwarmSource.node_id == uid))).scalars().all() == [] @pytest.mark.asyncio diff --git a/packages/meshbay-hub/tests/test_availability_between_members.py b/packages/meshbay-hub/tests/test_availability_between_members.py index 24d85a0..2773d87 100644 --- a/packages/meshbay-hub/tests/test_availability_between_members.py +++ b/packages/meshbay-hub/tests/test_availability_between_members.py @@ -202,56 +202,6 @@ async def test_the_notify_budget_is_not_refilled_by_reconnecting(client): rev._notify_window.pop(node_id, None) -# ── A member must not aim other people's traffic ───────────────────────────── - -@pytest.mark.asyncio -async def test_a_swarm_source_cannot_name_someone_elses_address(client): - """ - `endpoint` was free text documented as "ip:port", so an account could - publish a third party's address as a source for any content. Nothing dials - a swarm source today, which is the only reason this was not already the - reflection primitive that `notify_incoming` was fixed for (H6). A port is - all a reader needs: where the node is comes from the node record, which is - stamped with the address its announce arrived from. - """ - user = await _make_user(client, "av_swarm1") - headers = {"Authorization": f"Bearer {user['token']}"} - - for bad in ("192.0.2.7:4433", "evil.example:53", "webrtc:0", "webrtc:70000", - "webrtc:4433 ", "http://example.test"): - r = await client.post("/v1/swarm/register", headers=headers, - json={"content_hash": "ab" * 32, "endpoint": bad}) - assert r.status_code == 422, f"{bad!r} was accepted: {r.text}" - - r = await client.post("/v1/swarm/register", headers=headers, - json={"content_hash": "ab" * 32, "endpoint": "webrtc:19010"}) - assert r.status_code == 201, r.text - - -@pytest.mark.asyncio -async def test_one_account_cannot_fill_the_swarm_table(client, monkeypatch): - """Rows are keyed (hash, account) with no cap — an invented hash each time.""" - import meshbay_hub.api.groups as groups_api - monkeypatch.setattr(groups_api, "MAX_SWARM_HASHES_PER_ACCOUNT", 3) - - user = await _make_user(client, "av_swarm2") - headers = {"Authorization": f"Bearer {user['token']}"} - for i in range(3): - r = await client.post("/v1/swarm/register", headers=headers, - json={"content_hash": f"{i:064x}", - "endpoint": "webrtc:19010"}) - assert r.status_code == 201, r.text - - r = await client.post("/v1/swarm/register", headers=headers, - json={"content_hash": f"{99:064x}", "endpoint": "webrtc:19010"}) - assert r.status_code == 429, r.text - - # Refreshing one already held is not a new claim and must still work. - r = await client.post("/v1/swarm/register", headers=headers, - json={"content_hash": f"{0:064x}", "endpoint": "webrtc:19011"}) - assert r.status_code == 201, r.text - - # ── A member's node must not answer for another's ──────────────────────────── def test_a_node_cannot_answer_an_offer_it_was_never_sent(): diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 220a908..a29aa9f 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -667,12 +667,6 @@ class NodeDaemon(EnrichmentMixin): await indexer.initial_scan() log.info("Background scan complete for %s: %d files", name, indexer.index.count) - # Swarm registration for public groups (after files are known). - if gctx.get("visibility") == "public": - endpoint = f"webrtc:{self._config.node.quic_port}" - hashes = [e.id for e in gctx["index"].entries] - if hashes: - await self._register_swarm(hashes, endpoint) # initial_scan() itself never calls on_change (it predates # the concept — every existing caller only cared about the # scan finishing, not about notifying anyone) — but Videos @@ -1468,21 +1462,6 @@ class NodeDaemon(EnrichmentMixin): log.info("Index %s pushed to %d WebRTC peers", "delta" if delta is not None else "sync", pushed) - # 11.9 — Register file hashes with hub swarm table (public groups only, H7) - group_cfg = next( - (g for g in self._config.groups if g.id == group_id), None) - if (self._hub and self._state.get("endpoint_hint") - and group_cfg and group_cfg.visibility == "public"): - # Only the newly added hashes once there is a delta to know them - # from — registering the whole library again on every change is - # the same O(changes x library size) cost the delta above exists - # to avoid. - hashes = ([e.id for e in delta.additions] if delta is not None - else [e.id for e in idx.entries]) - if hashes: - endpoint = f"webrtc:{self._config.node.quic_port}" - spawn(self._register_swarm(hashes, endpoint)) - def _drop_group_sessions(self, group_id: str) -> None: """Close live sessions for a revoked group (H4).""" if not self._webrtc or not group_id: @@ -1492,13 +1471,6 @@ class NodeDaemon(EnrichmentMixin): spawn(session.close()) log.info("Dropped session for revoked group %s", group_id[:8]) - async def _register_swarm(self, hashes: list[str], endpoint: str) -> None: - try: - n = await self._hub.register_swarm(hashes, endpoint) - log.info("Swarm: registered %d/%d hashes", n, len(hashes)) - except Exception as e: - log.warning("Swarm registration failed: %s", e) - async def _shutdown(self) -> None: log.info("Shutting down...") self._state["status"] = "stopping" diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py index 346a4cd..5f47090 100644 --- a/packages/meshbay-node/src/meshbay_node/hub_client.py +++ b/packages/meshbay-node/src/meshbay_node/hub_client.py @@ -6,7 +6,6 @@ Handles all communication from the node to a Mesh Hub: - JWT offline verification and auto-refresh - Node announcement (endpoint_hint) - User public key lookup (for GEK wrapping) - - Swarm hash registration The node authenticates via Ed25519 challenge-response (/v1/nodes/auth). No auth_key or password is ever stored on or transmitted from the node. @@ -461,27 +460,6 @@ class HubClient: log.warning("Could not deliver WebRTC answer to %s: %s", str(msg.get("peer_id"))[:8], e) - # ── Swarm registration ───────────────────────────────────────────────── - - async def register_swarm(self, content_hashes: list[str], endpoint: str) -> int: - """Register file hashes in the hub swarm table. Returns count registered.""" - if self._session is None: - raise RuntimeError("Not logged in") - await self.ensure_fresh_token() - - registered = 0 - for h in content_hashes: - try: - r = await self._http.post("/v1/swarm/register", json={ - "content_hash": h, - "endpoint": endpoint, - }, headers=self._session.auth_headers) - if r.status_code in (201, 200): - registered += 1 - except Exception: - pass - return registered - # ── Convenience: full startup sequence ─────────────────────────────────── async def startup(self, endpoint_hint: str | None = None) -> HubSession: diff --git a/packages/meshbay-node/tests/test_daemon.py b/packages/meshbay-node/tests/test_daemon.py index 8c4da2d..acaafac 100644 --- a/packages/meshbay-node/tests/test_daemon.py +++ b/packages/meshbay-node/tests/test_daemon.py @@ -2,7 +2,7 @@ Integration test: Node daemon wires all components correctly. Phase 11 — verifies that NodeDaemon creates chat stores, WebRTC transport, -index push on change, swarm registration, and shuts down cleanly. +index push on change, and shuts down cleanly. Hub interaction is mocked. """ @@ -241,7 +241,6 @@ async def test_daemon_index_change_pushes_to_peers(tmp_path, shared_dir, gek, hu daemon = NodeDaemon(config) daemon._broadcast_coalesce_secs = 0.01 # real value would make this test wait 0.5s daemon._hub = AsyncMock() - daemon._hub.register_swarm = AsyncMock(return_value=2) daemon._state["endpoint_hint"] = "node123" sk_node = Ed25519PrivateKey.generate() @@ -268,47 +267,10 @@ async def test_daemon_index_change_pushes_to_peers(tmp_path, shared_dir, gek, hu payload = unseal(gek, PURPOSE_INDEX, "index_sync", "a" * 32, msg) assert len(payload["entries"]) == indexer.index.count - # Finding H7: this group is private, so its content hashes must NOT be - # registered with the hub. The test previously asserted the opposite — - # publishing a fingerprint of every private file was treated as expected - # behaviour. Index push to members is unaffected (asserted above). + # Finding H7: a change to the index tells the hub nothing — no content hash + # of any group reaches it. Index push to members is unaffected (above). await asyncio.sleep(0.1) - daemon._hub.register_swarm.assert_not_called() - -@pytest.mark.asyncio -async def test_daemon_index_change_registers_swarm_for_public_group( - tmp_path, shared_dir, gek, hub_pk_pem): - """Public groups still register content hashes with the hub swarm (H7).""" - config = Config( - hub=HubConfig(url="http://localhost:9999", username="testuser"), - node=NodeConfig(quic_port=_free_port(), ui_port=_free_port()), - groups=[GroupConfig( - id="a" * 32, - name="public-group", - shared_dir=str(shared_dir), - visibility="public", - quic_port=29010, - )], - keystore=KeystoreConfig(path=tmp_path / "keystore.enc"), - data_dir=tmp_path / "data", - ) - daemon = NodeDaemon(config) - daemon._broadcast_coalesce_secs = 0.01 - daemon._hub = AsyncMock() - daemon._hub.register_swarm = AsyncMock(return_value=2) - daemon._state["endpoint_hint"] = "node123" - - indexer = DirectoryIndexer( - roots=one_root(shared_dir), group_id="a" * 32, - sk_node=Ed25519PrivateKey.generate(), gek=gek) - await indexer.initial_scan() - - await daemon._on_index_change(indexer) - - await asyncio.sleep(0.1) - daemon._hub.register_swarm.assert_called_once() - assert len(daemon._hub.register_swarm.call_args[0][0]) == indexer.index.count - + assert daemon._hub.mock_calls == [] @pytest.mark.asyncio async def test_daemon_index_change_skips_other_group_peers( @@ -325,7 +287,6 @@ async def test_daemon_index_change_skips_other_group_peers( daemon = NodeDaemon(config) daemon._broadcast_coalesce_secs = 0.01 daemon._hub = AsyncMock() - daemon._hub.register_swarm = AsyncMock(return_value=0) daemon._state["endpoint_hint"] = "node123" sk_node = Ed25519PrivateKey.generate() @@ -370,7 +331,6 @@ def _new_daemon_for_group(tmp_path, shared_dir, gek, group_id="a" * 32, daemon = NodeDaemon(config) daemon._broadcast_coalesce_secs = 0.01 daemon._hub = AsyncMock() - daemon._hub.register_swarm = AsyncMock(return_value=0) daemon._state["endpoint_hint"] = "node123" return daemon @@ -532,29 +492,3 @@ async def test_a_burst_of_changes_produces_one_broadcast(tmp_path, shared_dir, g await asyncio.sleep(0.05) session._send.assert_called_once() - - -@pytest.mark.asyncio -async def test_swarm_registration_only_sends_new_hashes_after_the_first( - tmp_path, shared_dir, gek): - daemon = _new_daemon_for_group(tmp_path, shared_dir, gek, visibility="public") - indexer = DirectoryIndexer( - roots=one_root(shared_dir), group_id="a" * 32, - sk_node=Ed25519PrivateKey.generate(), gek=gek) - await indexer.initial_scan() - total_files = indexer.index.count - - await daemon._on_index_change(indexer) - await asyncio.sleep(0.05) - assert len(daemon._hub.register_swarm.call_args_list[0].args[0]) == total_files - - from meshbay_common.protocol import IndexEntry - indexer.index.add_entry(IndexEntry(id="new-file-id", name="new.mp4", - path="shared", size=10, type="video", - added_at=0)) - await daemon._on_index_change(indexer) - await asyncio.sleep(0.05) - - assert daemon._hub.register_swarm.call_count == 2 - assert daemon._hub.register_swarm.call_args_list[1].args[0] == ["new-file-id"], \ - "only the newly added hash must be (re-)registered, not the whole library" diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py index c0c2c2c..43e88c2 100644 --- a/packages/meshbay-node/tests/test_security_regressions.py +++ b/packages/meshbay-node/tests/test_security_regressions.py @@ -602,19 +602,18 @@ def test_denylist_persists_and_honours_groups(tmp_path): assert not reloaded.is_denied("someone", "other", "g-allowed") -def test_swarm_registration_skips_private_groups(): +def test_the_node_registers_no_content_hash_with_the_hub(): """ - H7: the daemon registered content hashes for every group, private included, - handing the hub a fingerprint of every private file. The bug was masked by a - mis-mounted route, so fixing the route without this filter would have turned a - dormant leak into a live one. + H7: the daemon once registered content hashes for every group, private + included, handing the hub a fingerprint of every private file. The swarm that + received them is gone, so no group's hashes are sent to the hub at all. """ - source = daemon_source() - assert 'visibility' in source and '_register_swarm' in source - # Both registration sites must gate on public visibility. - for marker in ['gctx.get("visibility") == "public"', - 'group_cfg.visibility == "public"']: - assert marker in source, f"swarm registration not gated: {marker}" + from pathlib import Path + + import meshbay_node.hub_client as hub_client + for source in (daemon_source(), Path(hub_client.__file__).read_text(encoding="utf-8")): + assert "/v1/swarm" not in source + assert "register_swarm" not in source def test_keystore_argon2_is_production_strength(): |