diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/revocation.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/revocation.py | 34 |
1 files changed, 33 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py index 1f6d7f0..d555f9f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py @@ -31,11 +31,12 @@ import json import logging import time import uuid +from datetime import datetime, timezone from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect from pydantic import BaseModel -from sqlalchemy import select +from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession import jwt @@ -67,6 +68,36 @@ def get_online_nodes_for_group(group_id: str) -> list[str]: return [nid for nid, gids in _node_groups.items() if group_id in gids] + +async def _mark_hosted(group_ids: list[str]) -> None: + """Stamp the first time a node announced it hosts each of these groups. + + `group_ids` is already narrowed to what this node may claim — the caller + derives it from the database and a node can only shrink the set, never widen + it (finding C2) — so being announced here is evidence the group has a host. + + Set once. A node going offline does not un-host a group, and re-stamping on + every reconnection would make `hosted_at` a "last seen" field, which is what + the in-memory registry is already for. + """ + from meshbay_hub.db.engine import get_session_factory + from meshbay_hub.db.models import Group + + if not group_ids: + return + try: + async with get_session_factory()() as db: + await db.execute( + update(Group) + .where(Group.id.in_(group_ids), Group.hosted_at.is_(None)) + .values(hosted_at=datetime.now(timezone.utc))) + await db.commit() + except Exception as e: + # A group that stays unhosted in the table is visible to its owner and + # collected later; failing the socket over it would take the node down. + log.warning("Could not mark groups hosted: %s", e) + + async def broadcast_revocation(token: str) -> int: """Push a signed revocation token to all connected nodes. Returns count sent.""" payload = json.dumps({"type": "revocation", "token": token}) @@ -236,6 +267,7 @@ async def node_websocket(ws: WebSocket): node_id = resolved_id _connected_nodes[node_id] = ws _node_groups[node_id] = group_ids + await _mark_hosted(group_ids) log.info("Node WS connected: %s (user=%s, groups=%d)", node_id[:8], user_id[:8], len(group_ids)) await ws.send_text(json.dumps({"type": "auth_ok", "node_id": node_id})) |