aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-28 21:35:20 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-28 21:35:20 +0200
commit91505face56f7ee6817408e52bad7902add75f09 (patch)
tree9902490872f441c5c88ea1f7ab57288ab6507899 /packages/meshbay-hub/src/meshbay_hub/api
parenta421a03d2be16670dc8d9076d26f4a7eac669986 (diff)
downloadmeshbay-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/meshbay-hub/src/meshbay_hub/api')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/groups.py108
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py7
2 files changed, 2 insertions, 113 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