From 86188385cbdae1ee90c1dca7a7b9db2edef1ecd4 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 19 Sep 2026 14:24:13 +0200 Subject: style: ruff's own fixes, mechanically applied MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ruff check .` had gone unrun long enough to report 568 errors, which is the same as having no linter: the next real finding would have been invisible in the noise. This is the 521 it fixes by itself, in 173 files, and nothing else — the 98 it cannot fix are the next commit. What actually changed: import sorting (225), imports nobody used (87, none of them a re-export — no `__init__.py` is touched, which was the one way this could have broken an import elsewhere), `datetime.timezone.utc` to `datetime.UTC` (69) and `asyncio.TimeoutError` to `TimeoutError` (18), both plain aliases on the 3.12 this project requires, `Optional[X]` to `X | None` (24), and f-strings with nothing to interpolate (19). Checked rather than assumed: every module in the three packages still imports, and the suite is 2893 passed — the same count, test for test, as the merge before it. Co-Authored-By: Claude Opus 5 --- packages/meshbay-hub/src/meshbay_hub/api/admin.py | 5 +-- packages/meshbay-hub/src/meshbay_hub/api/deps.py | 2 +- .../meshbay-hub/src/meshbay_hub/api/federation.py | 12 +++--- packages/meshbay-hub/src/meshbay_hub/api/groups.py | 21 ++++++---- packages/meshbay-hub/src/meshbay_hub/api/hub.py | 2 +- .../meshbay-hub/src/meshbay_hub/api/moderation.py | 1 - packages/meshbay-hub/src/meshbay_hub/api/nodes.py | 10 ++--- .../src/meshbay_hub/api/notifications.py | 6 +-- .../meshbay-hub/src/meshbay_hub/api/revocation.py | 20 ++++----- .../meshbay-hub/src/meshbay_hub/api/signaling.py | 4 +- packages/meshbay-hub/src/meshbay_hub/api/users.py | 49 +++++++++++++--------- packages/meshbay-hub/src/meshbay_hub/app.py | 42 ++++++++++--------- packages/meshbay-hub/src/meshbay_hub/auth.py | 4 +- packages/meshbay-hub/src/meshbay_hub/csam.py | 5 +-- .../meshbay-hub/src/meshbay_hub/db/__init__.py | 4 +- .../src/meshbay_hub/db/migrations/env.py | 6 +-- .../a7b8c9d0e1f2_add_group_last_activity.py | 11 +++-- .../versions/a9b8c7d6e5f4_add_login_throttle.py | 8 ++-- .../versions/b1c2d3e4f5a6_add_hub_settings.py | 11 +++-- .../c3d4e5f6a7b8_group_name_unique_per_owner.py | 11 +++-- .../versions/d28b9caf9f07_initial_schema.py | 11 +++-- .../d4e5f6a7b8c9_add_email_verification.py | 11 +++-- .../versions/e5f6a7b8c9d0_add_mail_quota.py | 8 ++-- .../versions/f1a2b3c4d5e6_add_user_preferences.py | 11 +++-- packages/meshbay-hub/src/meshbay_hub/db/models.py | 14 +++++-- .../meshbay-hub/src/meshbay_hub/login_throttle.py | 10 ++--- packages/meshbay-hub/src/meshbay_hub/mail.py | 24 ++++++----- .../meshbay-hub/src/meshbay_hub/tasks/cleanup.py | 10 ++--- 28 files changed, 173 insertions(+), 160 deletions(-) (limited to 'packages/meshbay-hub/src') diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py index 7ca1e68..d8e13e9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py @@ -6,19 +6,18 @@ Separate from moderation.py (which handles public reporting and content blocklis """ import logging -from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_hub.auth import decrypt_email +from meshbay_hub import hub_settings from meshbay_hub.api.deps import require_admin, require_moderator, user_is_admin from meshbay_hub.api.revocation import get_connected_node_count, is_node_connected +from meshbay_hub.auth import decrypt_email from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User -from meshbay_hub import hub_settings log = logging.getLogger(__name__) diff --git a/packages/meshbay-hub/src/meshbay_hub/api/deps.py b/packages/meshbay-hub/src/meshbay_hub/api/deps.py index 907a481..501be9d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/deps.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/deps.py @@ -8,8 +8,8 @@ JWT scope enforcement: """ from fastapi import Depends, Header, HTTPException, status -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub.auth import decode_access_token from meshbay_hub.db.engine import get_db diff --git a/packages/meshbay-hub/src/meshbay_hub/api/federation.py b/packages/meshbay-hub/src/meshbay_hub/api/federation.py index 9e252c6..755639e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/federation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/federation.py @@ -23,18 +23,18 @@ Protocol version: MHP 0.1 import logging import time import uuid +from datetime import UTC import jwt -from fastapi import APIRouter, Depends, HTTPException, Header +from fastapi import APIRouter, Depends, Header, HTTPException +from meshbay_common import MHP_VERSION from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_common import MHP_VERSION from meshbay_hub import __version__, hub_settings from meshbay_hub.api.deps import require_admin -from meshbay_hub.auth import ( - hub_id, hub_private_key_pem, hub_public_key_pem) +from meshbay_hub.auth import hub_id, hub_private_key_pem, hub_public_key_pem from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import FederatedGroup, Group, HubPeer, User @@ -244,8 +244,8 @@ async def receive_directory( if len(body.groups) > MAX_FEDERATED_GROUPS_PER_PUSH: raise HTTPException(status_code=413, detail="Too many groups in one push") - from datetime import datetime, timezone - now = datetime.now(timezone.utc) + from datetime import datetime + now = datetime.now(UTC) have = await db.scalar( select(func.count()).select_from(FederatedGroup) .where(FederatedGroup.source_hub == sender)) or 0 diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py index 72ba194..3a11345 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py @@ -1,22 +1,27 @@ """Group endpoints — /v1/groups/*""" import re +from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel -from datetime import datetime, timezone from sqlalchemy import func, or_, select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub import hub_settings, mail -from meshbay_hub.auth import decrypt_email from meshbay_hub.api.deps import get_current_user, require_user_scope from meshbay_hub.api.middleware import limiter from meshbay_hub.api.netutil import client_ip +from meshbay_hub.auth import decrypt_email from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import ( - FederatedGroup, Group, GroupMember, IPLog, SwarmSource, User, + FederatedGroup, + Group, + GroupMember, + IPLog, + SwarmSource, + User, ) router = APIRouter(prefix="/v1/groups", tags=["groups"]) @@ -97,7 +102,7 @@ async def touch_group_activity( await db.execute( update(Group) .where(Group.id == group_id) - .values(last_activity_at=datetime.now(timezone.utc))) + .values(last_activity_at=datetime.now(UTC))) await db.commit() return {"ok": True} @@ -261,9 +266,9 @@ async def swarm_register( detail="endpoint must be ':' — a port on the " "registering node, not an address") - from datetime import datetime, timezone + from datetime import datetime existing = await db.get(SwarmSource, (body.content_hash, current_user.id)) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) if existing: existing.endpoint = body.endpoint existing.last_seen = now @@ -297,8 +302,8 @@ async def swarm_sources( 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, timezone, timedelta - cutoff = datetime.now(timezone.utc) - timedelta(minutes=30) + from datetime import datetime, timedelta + cutoff = datetime.now(UTC) - timedelta(minutes=30) result = await db.execute( select(SwarmSource) .where( diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py index 94e9b3c..cab60c8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py @@ -1,9 +1,9 @@ """Hub info endpoints — /v1/hub/*""" from fastapi import APIRouter, Depends +from meshbay_common import MHP_VERSION, MNP_VERSION from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_common import MNP_VERSION, MHP_VERSION from meshbay_hub import __version__, hub_settings from meshbay_hub.api import federation from meshbay_hub.auth import hub_public_key_pem diff --git a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py index ee10cbc..35c688c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py @@ -23,7 +23,6 @@ Node integration: """ import logging -from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py index 83b60f2..7478173 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py @@ -1,20 +1,20 @@ """Node endpoints — /v1/nodes/*""" -from datetime import datetime, timezone import base64 import time +from datetime import UTC, datetime -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_hub.auth import issue_access_token from meshbay_hub.api.deps import get_current_user from meshbay_hub.api.middleware import limiter from meshbay_hub.api.netutil import client_ip +from meshbay_hub.auth import issue_access_token from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import GroupMember, IPLog, Node, User @@ -156,7 +156,7 @@ async def announce_node( if node is not None: node.endpoint_hint = body.endpoint_hint node.observed_ip = seen_from - node.last_seen = datetime.now(timezone.utc) + node.last_seen = datetime.now(UTC) db.add(IPLog(user_id=current_user.id, event="node_announce", ip_address=seen_from, detail=body.endpoint_hint)) await db.commit() @@ -182,7 +182,7 @@ async def announce_node( pk_node=body.pk_node, endpoint_hint=body.endpoint_hint, observed_ip=seen_from, - last_seen=datetime.now(timezone.utc), + last_seen=datetime.now(UTC), ) db.add(node) db.add(IPLog( diff --git a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py index b5783ab..d96ec18 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py @@ -19,9 +19,9 @@ migration for no gain, and `unread_only` stays because it is what an older interface asks for and it still answers correctly — every row is unread. """ -from fastapi import APIRouter, Depends, HTTPException, Query -from datetime import datetime, timezone +from datetime import UTC, datetime +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -182,7 +182,7 @@ async def create_notification( existing.detail = detail existing.link = link existing.read = False - existing.created_at = datetime.now(timezone.utc) + existing.created_at = datetime.now(UTC) await db.flush() return existing diff --git a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py index 1f1f5c5..f8cae8a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py @@ -26,23 +26,21 @@ and close active connections for that user. """ import asyncio -import base64 import json import logging import time import uuid -from datetime import datetime, timezone -from typing import Any +from datetime import UTC, datetime +import jwt from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect +from meshbay_common.background import spawn from pydantic import BaseModel from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession -import jwt -from meshbay_common.background import spawn -from meshbay_hub.auth import hub_public_key_pem, decode_access_token from meshbay_hub.api.deps import get_current_user, require_admin +from meshbay_hub.auth import decode_access_token from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User @@ -143,7 +141,7 @@ async def _mark_hosted(group_ids: list[str]) -> None: await db.execute( update(Group) .where(Group.id.in_(group_ids), Group.hosted_at.is_(None)) - .values(hosted_at=datetime.now(timezone.utc))) + .values(hosted_at=datetime.now(UTC))) await db.commit() except Exception as e: # A group that stays unhosted in the table is visible to its owner and @@ -169,7 +167,7 @@ async def broadcast_revocation(token: str) -> int: def _sign_revocation(target: str, target_id: str, reason: str) -> str: """Issue a signed revocation token (JWT EdDSA).""" - from meshbay_hub.auth import _hub_sk_pem, _hub_id + from meshbay_hub.auth import _hub_id, _hub_sk_pem now = int(time.time()) payload = { "type": "revocation", @@ -208,9 +206,9 @@ async def _handle_chat_notify(group_id: str, sender_name: str, sender_user_id: s (node_id or "?")[:8]) return try: - from meshbay_hub.db.engine import get_session_factory - from meshbay_hub.db.models import GroupMember, Group from meshbay_hub.api.notifications import create_notification + from meshbay_hub.db.engine import get_session_factory + from meshbay_hub.db.models import Group, GroupMember async with get_session_factory()() as db: group = await db.get(Group, group_id) @@ -478,7 +476,7 @@ async def notify_incoming( try: await asyncio.wait_for(event.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: raise HTTPException(status_code=504, detail="Node did not respond in time") finally: _punch_events.pop(node_id, None) diff --git a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py index bc03b45..8a10822 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py @@ -25,8 +25,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub import hub_settings from meshbay_hub.api.deps import get_current_user from meshbay_hub.api.middleware import limiter -from meshbay_hub.db.engine import get_db from meshbay_hub.api.netutil import client_ip +from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import Group, GroupMember, IPLog, User log = logging.getLogger(__name__) @@ -167,7 +167,7 @@ async def webrtc_offer( try: answer = await asyncio.wait_for(answer_future, timeout=15.0) - except asyncio.TimeoutError: + except TimeoutError: raise HTTPException( status_code=504, detail="Node did not respond with WebRTC answer") diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index 0394f53..ee8aabc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -6,7 +6,7 @@ import re import secrets import time import uuid -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from fastapi import APIRouter, Depends, HTTPException, Request @@ -33,8 +33,17 @@ from meshbay_hub.auth import ( from meshbay_hub.config import HubConfig from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import ( - EmailVerification, Group, GroupMember, IPLog, Node, Notification, - RefreshToken, SwarmSource, User, UserDevice, UserPreference, + EmailVerification, + Group, + GroupMember, + IPLog, + Node, + Notification, + RefreshToken, + SwarmSource, + User, + UserDevice, + UserPreference, ) log = logging.getLogger(__name__) @@ -61,7 +70,7 @@ async def _refresh_expiry(db: AsyncSession, family_id: str | None = None) -> dat renewals of a tab that is being used. """ limits = await hub_settings.session_limits(db) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) idle = max(limits["refresh_idle_hours"] * 3600, _ttl() + 3600) started = now if family_id is not None: @@ -69,7 +78,7 @@ async def _refresh_expiry(db: AsyncSession, family_id: str | None = None) -> dat select(func.min(RefreshToken.created_at)) .where(RefreshToken.family_id == family_id)) if first is not None: - started = first if first.tzinfo else first.replace(tzinfo=timezone.utc) + started = first if first.tzinfo else first.replace(tzinfo=UTC) return min(now + timedelta(seconds=idle), started + timedelta(hours=limits["max_hours"])) @@ -193,7 +202,7 @@ async def register( EmailVerification.user_id == found.id, EmailVerification.purpose == "registration", EmailVerification.created_at - > datetime.now(timezone.utc) + > datetime.now(UTC) - timedelta(seconds=resend_cooldown), )) if not recent.first(): @@ -270,7 +279,7 @@ async def _create_and_send_verification( code=code, purpose="registration", user_id=user.id, - expires_at=datetime.now(timezone.utc) + timedelta(seconds=VERIFICATION_TTL), + expires_at=datetime.now(UTC) + timedelta(seconds=VERIFICATION_TTL), )) await db.flush() await mail.send_off_loop( @@ -292,7 +301,7 @@ async def verify_email( ): """Verify a registration email with the code received by mail.""" eh = hash_email_blind(body.email) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) result = await db.execute( select(EmailVerification).where( @@ -306,7 +315,7 @@ async def verify_email( raise HTTPException(status_code=404, detail="No pending verification for this email") - if verif.expires_at.replace(tzinfo=timezone.utc) < now: + if verif.expires_at.replace(tzinfo=UTC) < now: raise HTTPException(status_code=410, detail="Verification code expired") if verif.attempts >= VERIFICATION_MAX_ATTEMPTS: @@ -603,7 +612,7 @@ async def device_auth( await db.commit() raise HTTPException(status_code=401, detail="Invalid signature") - matched.last_seen = datetime.now(timezone.utc) + matched.last_seen = datetime.now(UTC) memberships = await db.execute( select(GroupMember.group_id).where(GroupMember.user_id == user.id)) @@ -650,7 +659,7 @@ async def token_refresh( await db.commit() raise HTTPException(status_code=401, detail="Token reuse detected — family revoked") - if rt.expires_at.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc): + if rt.expires_at.replace(tzinfo=UTC) < datetime.now(UTC): raise HTTPException(status_code=401, detail="Expired refresh token") user = await db.get(User, rt.user_id) @@ -659,7 +668,7 @@ async def token_refresh( # The family's first sign-in was longer ago than any session may last. expires_at = await _refresh_expiry(db, rt.family_id) - if expires_at <= datetime.now(timezone.utc): + if expires_at <= datetime.now(UTC): await db.execute( update(RefreshToken) .where(RefreshToken.family_id == rt.family_id) @@ -779,7 +788,7 @@ async def update_profile( cooldown = await hub_settings.get_int( db, "mail.email_change_cooldown", hub_settings.mail_default("email_change_cooldown")) - since = datetime.now(timezone.utc) - timedelta(seconds=cooldown) + since = datetime.now(UTC) - timedelta(seconds=cooldown) recent = await db.execute( select(IPLog).where( IPLog.user_id == current_user.id, @@ -820,7 +829,7 @@ async def update_profile( code=code, purpose="email_change", user_id=current_user.id, - expires_at=datetime.now(timezone.utc) + timedelta(seconds=VERIFICATION_TTL), + expires_at=datetime.now(UTC) + timedelta(seconds=VERIFICATION_TTL), )) db.add(IPLog(user_id=current_user.id, event="email_change_request", ip_address=client_ip(request))) @@ -860,7 +869,7 @@ async def verify_email_change( db: AsyncSession = Depends(get_db), ): """Confirm an email change with the code sent to the new address.""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) result = await db.execute( select(EmailVerification).where( @@ -874,7 +883,7 @@ async def verify_email_change( raise HTTPException(status_code=404, detail="No pending email change") - if verif.expires_at.replace(tzinfo=timezone.utc) < now: + if verif.expires_at.replace(tzinfo=UTC) < now: raise HTTPException(status_code=410, detail="Verification code expired") if verif.attempts >= VERIFICATION_MAX_ATTEMPTS: @@ -1090,7 +1099,7 @@ async def password_reset_request( EmailVerification.user_id == user.id, EmailVerification.purpose == "password_reset", EmailVerification.created_at - > datetime.now(timezone.utc) - timedelta(seconds=reset_cooldown), + > datetime.now(UTC) - timedelta(seconds=reset_cooldown), )) if recent.first(): return {"status": "sent_if_exists"} @@ -1110,7 +1119,7 @@ async def password_reset_request( code=code, purpose="password_reset", user_id=user.id, - expires_at=datetime.now(timezone.utc) + expires_at=datetime.now(UTC) + timedelta(seconds=PASSWORD_RESET_TTL), )) db.add(IPLog(user_id=user.id, event="password_reset_request", @@ -1141,7 +1150,7 @@ async def password_reset( request: Request, db: AsyncSession = Depends(get_db), ): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) result = await db.execute(select(User).where(User.username == body.username)) user = result.scalar_one_or_none() if not user: @@ -1157,7 +1166,7 @@ async def password_reset( if not verif: raise HTTPException(status_code=404, detail="No pending reset for this account") - if verif.expires_at.replace(tzinfo=timezone.utc) < now: + if verif.expires_at.replace(tzinfo=UTC) < now: raise HTTPException(status_code=410, detail="Reset code expired") if verif.attempts >= VERIFICATION_MAX_ATTEMPTS: raise HTTPException(status_code=429, detail="Too many attempts") diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py index afc1cde..209dc32 100644 --- a/packages/meshbay-hub/src/meshbay_hub/app.py +++ b/packages/meshbay-hub/src/meshbay_hub/app.py @@ -11,37 +11,41 @@ Usage: import asyncio from contextlib import asynccontextmanager -from pathlib import Path from fastapi import FastAPI from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from meshbay_hub import __version__ +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 +from meshbay_hub.api.middleware import limiter +from meshbay_hub.api.moderation import router as moderation_router +from meshbay_hub.api.nodes import router as nodes_router +from meshbay_hub.api.notifications import router as notifications_router +from meshbay_hub.api.relay import router as relay_router +from meshbay_hub.api.revocation import router as revocation_router +from meshbay_hub.api.signaling import router as signaling_router +from meshbay_hub.api.users import router as users_router +from meshbay_hub.api.users import set_config as users_set_config +from meshbay_hub.api.webapp import ASSET_V, CSP, STATIC_DIR +from meshbay_hub.api.webapp import router as webapp_router from meshbay_hub.auth import generate_hub_keypair, load_hub_keypair from meshbay_hub.config import HubConfig +from meshbay_hub.csam import csam_router from meshbay_hub.db.engine import close_db, init_db -from meshbay_hub.api.hub import router as hub_router, set_config as hub_set_config -from meshbay_hub.api.users import router as users_router, set_config as users_set_config -from meshbay_hub.api.deps import set_admin_usernames -from meshbay_hub.api.nodes import router as nodes_router -from meshbay_hub.api.groups import router as groups_router, swarm_router -from meshbay_hub.api.revocation import router as revocation_router -from meshbay_hub.api.moderation import router as moderation_router -from meshbay_hub.api.federation import router as federation_router -from meshbay_hub.csam import csam_router -from meshbay_hub.api.health import router as health_router -from meshbay_hub.api.relay import router as relay_router -from meshbay_hub.api.signaling import router as signaling_router -from meshbay_hub.api.admin import router as admin_router -from meshbay_hub.api.notifications import router as notifications_router -from meshbay_hub.api.webapp import router as webapp_router, STATIC_DIR, ASSET_V, CSP -from meshbay_hub.api.middleware import limiter async def _sync_admin_roles(admin_usernames: list[str]) -> None: """Ensure config-listed admin usernames have role='admin' in the DB.""" - from sqlalchemy import select, update + from sqlalchemy import select + from meshbay_hub.db.engine import get_session_factory from meshbay_hub.db.models import User @@ -122,8 +126,8 @@ def create_app(cfg: HubConfig | None = None) -> FastAPI: from meshbay_hub.csam import get_csam_checker get_csam_checker().load() - from meshbay_hub.tasks.cleanup import cleanup_loop from meshbay_hub.db.engine import get_session_factory + from meshbay_hub.tasks.cleanup import cleanup_loop cleanup_task = asyncio.create_task(cleanup_loop(get_session_factory())) yield diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index 7045197..d038027 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -18,12 +18,12 @@ from pathlib import Path import blake3 import jwt -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.kdf.argon2 import Argon2id from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives.hashes import SHA256 # Argon2id parameters — versioned for gradual migration _ARGON2_LANES = 4 diff --git a/packages/meshbay-hub/src/meshbay_hub/csam.py b/packages/meshbay-hub/src/meshbay_hub/csam.py index 2a0ce25..e540068 100644 --- a/packages/meshbay-hub/src/meshbay_hub/csam.py +++ b/packages/meshbay-hub/src/meshbay_hub/csam.py @@ -19,9 +19,7 @@ IMPORTANT: Never log matched hashes or file contents. CSAM detection must be reported to NCMEC (US law) or relevant authority immediately. """ -import hashlib import logging -import os from pathlib import Path log = logging.getLogger(__name__) @@ -123,7 +121,8 @@ def check_content_hash(blake3_hex: str) -> bool: # ── Hub API integration ─────────────────────────────────────────────────────── -from fastapi import APIRouter, Depends, HTTPException, UploadFile, File +from fastapi import APIRouter, Depends, HTTPException + from meshbay_hub.api.deps import require_admin from meshbay_hub.db.models import User diff --git a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py index 62e5388..9d7483a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py @@ -1,6 +1,6 @@ """Hub database layer.""" -from .engine import init_db, close_db, get_db -from .models import Base, User, Node, Group, GroupMember, RefreshToken, IPLog +from .engine import close_db, get_db, init_db +from .models import Base, Group, GroupMember, IPLog, Node, RefreshToken, User __all__ = [ "init_db", "close_db", "get_db", diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py index 8908deb..61f80be 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py @@ -4,14 +4,12 @@ import asyncio import os from logging.config import fileConfig +from alembic import context +from meshbay_hub.db.models import Base from sqlalchemy import pool from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import async_engine_from_config -from alembic import context - -from meshbay_hub.db.models import Base - config = context.config if config.config_file_name is not None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py index 8a4e2ee..034d12f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py @@ -5,16 +5,15 @@ Revises: f1a2b3c4d5e6 Create Date: 2026-08-20 20:50:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'a7b8c9d0e1f2' -down_revision: Union[str, Sequence[str], None] = 'f1a2b3c4d5e6' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'f1a2b3c4d5e6' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py index 2fead6c..45985e2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py @@ -7,15 +7,15 @@ Revision ID: a9b8c7d6e5f4 Revises: e5f6a7b8c9d0 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "a9b8c7d6e5f4" -down_revision: Union[str, Sequence[str], None] = "e5f6a7b8c9d0" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = "e5f6a7b8c9d0" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py index 13f2b5c..9a90f7a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py @@ -5,16 +5,15 @@ Revises: a7b8c9d0e1f2 Create Date: 2026-08-28 12:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'b1c2d3e4f5a6' -down_revision: Union[str, Sequence[str], None] = 'a7b8c9d0e1f2' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'a7b8c9d0e1f2' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py index 6fc5b73..fea0583 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py @@ -10,16 +10,15 @@ its UUID — this only makes `name@owner` a dependable handle. Pre-flight: abort if the data already violates it, with the offending (admin_id, name) pairs listed, rather than silently renaming anyone's group. """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'c3d4e5f6a7b8' -down_revision: Union[str, Sequence[str], None] = 'b1c2d3e4f5a6' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'b1c2d3e4f5a6' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py index 6301426..a814f37 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py @@ -12,16 +12,15 @@ Revises: Create Date: 2026-08-09 04:35:07.120021 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'd28b9caf9f07' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py index 6741eb8..aa15b6d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py @@ -9,16 +9,15 @@ Revises: c3d4e5f6a7b8 Create Date: 2026-08-31 14:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'd4e5f6a7b8c9' -down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'c3d4e5f6a7b8' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py index 8f2e4d2..c729de4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py @@ -8,15 +8,15 @@ Revision ID: e5f6a7b8c9d0 Revises: d4e5f6a7b8c9 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "e5f6a7b8c9d0" -down_revision: Union[str, Sequence[str], None] = "d4e5f6a7b8c9" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = "d4e5f6a7b8c9" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py index e30a4da..9f8e729 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py @@ -5,16 +5,15 @@ Revises: d28b9caf9f07 Create Date: 2026-08-19 12:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'f1a2b3c4d5e6' -down_revision: Union[str, Sequence[str], None] = 'd28b9caf9f07' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'd28b9caf9f07' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py index 62e4a11..38c4723 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/models.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py @@ -11,17 +11,23 @@ Tables: """ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import ( - Boolean, DateTime, ForeignKey, Index, Integer, - String, Text, UniqueConstraint, text, + Boolean, + DateTime, + ForeignKey, + Index, + Integer, + String, + Text, + text, ) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship def _now() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) def _uuid() -> str: return str(uuid.uuid4()) diff --git a/packages/meshbay-hub/src/meshbay_hub/login_throttle.py b/packages/meshbay-hub/src/meshbay_hub/login_throttle.py index 3281088..bfd142e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/login_throttle.py +++ b/packages/meshbay-hub/src/meshbay_hub/login_throttle.py @@ -24,7 +24,7 @@ locks somebody else's name from signing them out (§13.5b, AV26). """ import hashlib -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from sqlalchemy import case, delete, select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -39,7 +39,7 @@ def _key(username: str) -> str: def _aware(dt: datetime) -> datetime: # SQLite hands back naive datetimes for a timezone-aware column. - return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc) + return dt if dt.tzinfo is not None else dt.replace(tzinfo=UTC) def _insert_for(db: AsyncSession): @@ -64,7 +64,7 @@ async def reserve(db: AsyncSession, username: str) -> tuple[bool, int]: if max_failures == 0: return True, 0 - now = datetime.now(timezone.utc) + now = datetime.now(UTC) window = timedelta(minutes=limits["lockout_minutes"]) window_start = now - window key = _key(username) @@ -102,7 +102,7 @@ async def locked_for(db: AsyncSession, username: str) -> int: return 0 remaining = (_aware(row.last_failure_at) + timedelta(minutes=limits["lockout_minutes"]) - - datetime.now(timezone.utc)).total_seconds() + - datetime.now(UTC)).total_seconds() return max(0, int(remaining + 0.999)) @@ -136,7 +136,7 @@ async def clear(db: AsyncSession, username: str) -> None: async def purge_expired(db: AsyncSession) -> int: """Rows whose failures have aged out. Every unknown name typed creates one.""" limits = await hub_settings.login_limits(db) - cutoff = datetime.now(timezone.utc) - timedelta(minutes=limits["lockout_minutes"]) + cutoff = datetime.now(UTC) - timedelta(minutes=limits["lockout_minutes"]) result = await db.execute( delete(LoginThrottle).where(LoginThrottle.last_failure_at < cutoff)) await db.commit() diff --git a/packages/meshbay-hub/src/meshbay_hub/mail.py b/packages/meshbay-hub/src/meshbay_hub/mail.py index 126ed45..1eb7c51 100644 --- a/packages/meshbay-hub/src/meshbay_hub/mail.py +++ b/packages/meshbay-hub/src/meshbay_hub/mail.py @@ -16,7 +16,7 @@ import asyncio import hashlib import logging import smtplib -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from email.message import EmailMessage log = logging.getLogger(__name__) @@ -79,7 +79,7 @@ async def _take(db, key: str, window: timedelta, ceiling: int, """ from meshbay_hub.db.models import MailQuota - now = datetime.now(timezone.utc) + now = datetime.now(UTC) row = await db.get(MailQuota, key) if row is None: row = MailQuota(key=key, window_start=now, count=0, last_sent=None) @@ -87,14 +87,14 @@ async def _take(db, key: str, window: timedelta, ceiling: int, started = row.window_start if started.tzinfo is None: - started = started.replace(tzinfo=timezone.utc) + started = started.replace(tzinfo=UTC) if now - started >= window: row.window_start, row.count = now, 0 if cooldown is not None and row.last_sent is not None: last = row.last_sent if last.tzinfo is None: - last = last.replace(tzinfo=timezone.utc) + last = last.replace(tzinfo=UTC) if now - last < cooldown: raise MailRefused("too soon since the last message to this recipient") @@ -131,12 +131,12 @@ async def _announce_exhaustion(scope: str) -> None: try: async with get_session_factory()() as db: key = f"alert:{scope}" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) row = await db.get(MailQuota, key) if row is not None and row.last_sent is not None: last = row.last_sent if last.tzinfo is None: - last = last.replace(tzinfo=timezone.utc) + last = last.replace(tzinfo=UTC) if now - last < timedelta(hours=1): return if row is None: @@ -202,9 +202,10 @@ async def reserve(db, purpose: str, address: str) -> None: async def status(db) -> dict: """What the operator sees in the panel: is the hub still sending?""" + from sqlalchemy import func, select + from meshbay_hub import hub_settings from meshbay_hub.db.models import MailQuota - from sqlalchemy import func, select limits = await hub_settings.mail_limits(db) row = await db.get(MailQuota, "hour") @@ -213,8 +214,8 @@ async def status(db) -> dict: if row is not None: started = row.window_start if started.tzinfo is None: - started = started.replace(tzinfo=timezone.utc) - if datetime.now(timezone.utc) - started < timedelta(hours=1): + started = started.replace(tzinfo=UTC) + if datetime.now(UTC) - started < timedelta(hours=1): used, window_start = row.count, started.isoformat() recipients = await db.scalar( @@ -244,10 +245,11 @@ async def status(db) -> dict: async def purge_expired_quota(db) -> int: """Drop counters whose window has passed. Returns how many went.""" - from meshbay_hub.db.models import MailQuota from sqlalchemy import delete - cutoff = datetime.now(timezone.utc) - timedelta(days=1) + from meshbay_hub.db.models import MailQuota + + cutoff = datetime.now(UTC) - timedelta(days=1) result = await db.execute( delete(MailQuota).where(MailQuota.window_start < cutoff)) await db.commit() diff --git a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py index 5c52387..429575f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py +++ b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py @@ -2,7 +2,7 @@ import asyncio import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession @@ -16,7 +16,7 @@ CLEANUP_INTERVAL_HOURS = 24 async def purge_old_ip_logs(db: AsyncSession, retention_days: int = RETENTION_DAYS) -> int: - cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days) + cutoff = datetime.now(UTC) - timedelta(days=retention_days) result = await db.execute(delete(IPLog).where(IPLog.timestamp < cutoff)) await db.commit() return result.rowcount @@ -26,7 +26,7 @@ PENDING_USER_EXPIRY_DAYS = 7 async def purge_expired_verifications(db: AsyncSession) -> int: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) result = await db.execute( delete(EmailVerification).where(EmailVerification.expires_at < now)) await db.commit() @@ -35,7 +35,7 @@ async def purge_expired_verifications(db: AsyncSession) -> int: async def purge_stale_pending_users(db: AsyncSession, expiry_days: int = PENDING_USER_EXPIRY_DAYS) -> int: - cutoff = datetime.now(timezone.utc) - timedelta(days=expiry_days) + cutoff = datetime.now(UTC) - timedelta(days=expiry_days) result = await db.execute( delete(User).where(User.status == "pending", User.created_at < cutoff)) await db.commit() @@ -93,7 +93,7 @@ async def find_unhosted_groups(db: AsyncSession, grace_days: int = UNHOSTED_GRAC whole reason the column exists rather than a check against the live socket registry, which would delete every group during a hub restart. """ - cutoff = datetime.now(timezone.utc) - timedelta(days=grace_days) + cutoff = datetime.now(UTC) - timedelta(days=grace_days) result = await db.execute( select(Group).where(Group.hosted_at.is_(None), Group.created_at < cutoff)) return list(result.scalars().all()) -- cgit v1.2.3 From 9e7b75bb0f6f6649fb00f2dc97059e90b7d52875 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 19 Sep 2026 14:39:38 +0200 Subject: style: the 98 ruff could not fix, so the linter is a signal again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pass before this applied ruff's own fixes. These are the ones needing a decision, and the point of doing them is that `ruff check .` now passes: a linter reporting 98 known-acceptable findings reports nothing, because the next real one arrives invisible. **Lines over 100 (70).** Mostly wrapped where they stood. Two exceptions: the aligned trailing comments in `protocol.py`'s message table were shortened rather than wrapped, because wrapping one row of a table breaks the table; and in `models.py` the column comments moved above their columns for the same reason. **Imports below the first statement (14).** `csam.py` kept its FastAPI imports under a section header halfway down the file; two node tests had a constant and a `pytestmark` wedged between two import blocks. Moved, not suppressed. **Bindings nothing reads (4).** Three in tests, where the call stays and only the name goes — `_user(client, "listener")` is there to create the user, not to return one. The fourth was in `revocation.py` and was not a lint finding at all: `_connect_and_listen` opened an httpx stream to the WebSocket URL, did `pass`, and then opened the real connection through the `websockets` library. One pointless request per connect, left over from before that library was used directly. Removed, and `httpx` with it. **`l` as a name (4)**, **semicolons (6)** in the POC spikes, and the rest. 2893 passed, the same count as the two commits before it. `meshbay_node/revocation.py` is worth a decision separately: 154 lines that nothing imports, superseded by `hub_client.maintain_ws`'s `on_revocation`. This commit only stopped it failing the linter. Co-Authored-By: Claude Opus 5 --- .../meshbay-common/src/meshbay_common/protocol.py | 27 +++++++++++----------- packages/meshbay-hub/src/meshbay_hub/api/admin.py | 11 ++++++--- packages/meshbay-hub/src/meshbay_hub/api/relay.py | 3 ++- packages/meshbay-hub/src/meshbay_hub/api/users.py | 4 +++- packages/meshbay-hub/src/meshbay_hub/api/webapp.py | 3 ++- packages/meshbay-hub/src/meshbay_hub/csam.py | 10 ++++---- packages/meshbay-hub/src/meshbay_hub/db/models.py | 17 +++++++++----- .../meshbay-hub/src/meshbay_hub/tasks/cleanup.py | 3 ++- .../meshbay-hub/tests/harness/boot_guard_probe.py | 3 ++- .../meshbay-hub/tests/harness/menu_scroll_probe.py | 3 ++- .../meshbay-hub/tests/harness/music_grid_probe.py | 6 +++-- .../meshbay-hub/tests/harness/music_queue_probe.py | 3 ++- .../tests/harness/playlist_store_probe.py | 6 +++-- .../meshbay-hub/tests/harness/playlist_ui_probe.py | 12 ++++++---- .../meshbay-hub/tests/test_files_drop_upload.py | 3 ++- packages/meshbay-hub/tests/test_hook_ordering.py | 3 ++- packages/meshbay-hub/tests/test_hub_api.py | 16 +++++++++---- packages/meshbay-hub/tests/test_layout_measured.py | 6 +++-- packages/meshbay-hub/tests/test_locales.py | 9 +++++--- packages/meshbay-hub/tests/test_memory_ceiling.py | 6 ++--- .../tests/test_notifications_behaviour.py | 2 +- packages/meshbay-hub/tests/test_transfers.py | 5 +++- packages/meshbay-node/src/meshbay_node/daemon.py | 3 ++- .../src/meshbay_node/indexer/title_parse.py | 3 ++- .../meshbay-node/src/meshbay_node/revocation.py | 11 +++------ .../meshbay-node/src/meshbay_node/transfers.py | 2 +- .../src/meshbay_node/transport/webrtc_server.py | 22 ++++++++++++------ .../meshbay-node/tests/test_audio_transcode.py | 6 +++-- packages/meshbay-node/tests/test_enrich_photo.py | 3 ++- packages/meshbay-node/tests/test_indexer.py | 3 ++- packages/meshbay-node/tests/test_multi_group.py | 6 +++-- packages/meshbay-node/tests/test_node_status.py | 1 - packages/meshbay-node/tests/test_packaging_win.py | 5 ++-- packages/meshbay-node/tests/test_roster_pairing.py | 3 ++- .../tests/test_security_regressions.py | 3 ++- .../meshbay-node/tests/test_transfer_slots_wire.py | 7 +++--- .../meshbay-node/tests/test_webrtc_transport.py | 19 ++++++++------- poc/spike1_crypto.py | 3 ++- poc/spike3_node.py | 3 ++- poc/spike4_nat.py | 7 ++++-- poc/spike5_client.py | 3 ++- poc/spike5_node.py | 3 ++- poc/spike6_gek.py | 7 ++++-- 43 files changed, 175 insertions(+), 109 deletions(-) (limited to 'packages/meshbay-hub/src') diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py index d6e30e7..5374742 100644 --- a/packages/meshbay-common/src/meshbay_common/protocol.py +++ b/packages/meshbay-common/src/meshbay_common/protocol.py @@ -68,7 +68,8 @@ class MNP: # and no client: removed rather than repaired. # # Not a Double Ratchet message, and never was — finding C1 - # (`docs/MESHBAY_DESIGN.md` §13.1) rejected exactly that for groups. Since MNP 2.0 it is AES-256-GCM under a + # (`docs/MESHBAY_DESIGN.md` §13.1) rejected exactly that for groups. Since + # MNP 2.0 it is AES-256-GCM under a # per-device subkey of the group's chat epoch key, signed over the # ciphertext with the sending device's pinned Ed25519 key. There is no # plaintext form on the wire (`chatbox.py`, docs/MESHBAY_DESIGN.md §4.5); @@ -158,21 +159,21 @@ class MNP: MEMBER_UNPIN_ACK = "member_unpin_ack" APPS_ENABLED = "apps_enabled" # operator → node: which group apps to show APPS_ENABLED_ACK = "apps_enabled_ack" - TRANSFER_LIMITS = "transfer_limits" # operator → node: per-member caps for this group + TRANSFER_LIMITS = "transfer_limits" # operator → node: per-member caps here TRANSFER_LIMITS_ACK = "transfer_limits_ack" # node → this group: the new caps SET_SCAN_SETTINGS = "set_scan_settings" # operator → node: reconcile/debounce timing SET_SCAN_SETTINGS_ACK = "set_scan_settings_ack" MEDIA_META_REQ = "media_meta_req" # client → node: TMDB metadata for a path MEDIA_META_RESP = "media_meta_resp" # node → client: TMDB metadata (or none) - TMDB_CONFIG = "tmdb_config" # operator → node: set custom TMDB token/language (node-wide) - TMDB_CONFIG_ACK = "tmdb_config_ack" # node → everyone: new TMDB config (never the token) - TMDB_ENABLED = "tmdb_enabled" # operator → node: enable/disable TMDB for this group - TMDB_ENABLED_ACK = "tmdb_enabled_ack" # node → this group: new per-group TMDB enabled state - SEASON_META_REQ = "season_meta_req" # client → node: TMDB overview/poster for one season - SEASON_META_RESP = "season_meta_resp" # node → client: season-level TMDB fields (or none) - TMDB_SEARCH_REQ = "tmdb_search_req" # client → node: candidate TMDB matches for a query - TMDB_SEARCH_RESP = "tmdb_search_resp" # node → client: candidate list (id, title, year, poster) - TMDB_OVERRIDE = "tmdb_override" # operator → node: replace a show/movie's TMDB match + TMDB_CONFIG = "tmdb_config" # operator → node: token/language, node-wide + TMDB_CONFIG_ACK = "tmdb_config_ack" # node → everyone: config, never the token + TMDB_ENABLED = "tmdb_enabled" # operator → node: TMDB on/off here + TMDB_ENABLED_ACK = "tmdb_enabled_ack" # node → this group: TMDB on/off here + SEASON_META_REQ = "season_meta_req" # client → node: one season's overview + SEASON_META_RESP = "season_meta_resp" # node → client: season fields, or none + TMDB_SEARCH_REQ = "tmdb_search_req" # client → node: candidates for a query + TMDB_SEARCH_RESP = "tmdb_search_resp" # node → client: id, title, year, poster + TMDB_OVERRIDE = "tmdb_override" # operator → node: replace a match TMDB_OVERRIDE_ACK = "tmdb_override_ack" TMDB_REMATCH = "tmdb_rematch" # operator → node: drop one file's match TMDB_REMATCH_ACK = "tmdb_rematch_ack" @@ -255,7 +256,7 @@ class MNP: # key without having to reconnect. CHAT_EPOCH = "chat_epoch" CHAT_EPOCH_ACK = "chat_epoch_ack" - ROOT_UPDATE = "root_update" # operator → node: change writable/removable on a root + ROOT_UPDATE = "root_update" # operator → node: a root's flags ROOT_UPDATE_ACK = "root_update_ack" ROOT_EJECT = "root_eject" # operator → node: mark removable root as ejected ROOT_EJECT_ACK = "root_eject_ack" @@ -269,7 +270,7 @@ class MNP: # node's `sender_id` (Tier 2, docs/MESHBAY_DESIGN.md §3.3). GROUP_ROSTER_REQ = "group_roster_req" GROUP_ROSTER_RESP = "group_roster_resp" - ROSTER_READ = "roster_read" # operator → node: list pinned identities + members + ROSTER_READ = "roster_read" # operator → node: identities + members ROSTER_READ_ACK = "roster_read_ack" DENYLIST_READ = "denylist_read" # operator → node: show denylist entries DENYLIST_READ_ACK = "denylist_read_ack" diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py index d8e13e9..b4b2f4f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py @@ -317,7 +317,8 @@ async def admin_patch_user( if body.role not in ("user", "moderator", "admin"): raise HTTPException(status_code=422, detail="role must be user, moderator, or admin") user.role = body.role - log.info("User %s role changed to %s by %s", user.username, body.role, current_user.username) + log.info("User %s role changed to %s by %s", + user.username, body.role, current_user.username) await create_notification( db, user.id, "role_change", f"Your role has been changed to {body.role}", @@ -325,7 +326,9 @@ async def admin_patch_user( if body.status is not None: if body.status not in ("active", "suspended", "revoked"): - raise HTTPException(status_code=422, detail="status must be active, suspended, or revoked") + raise HTTPException( + status_code=422, + detail="status must be active, suspended, or revoked") user.status = body.status log.info("User %s status changed to %s by %s", user.username, body.status, current_user.username) @@ -483,7 +486,9 @@ async def admin_patch_group( if body.status is not None: if body.status not in ("active", "suspended", "revoked"): - raise HTTPException(status_code=422, detail="status must be active, suspended, or revoked") + raise HTTPException( + status_code=422, + detail="status must be active, suspended, or revoked") group.status = body.status log.info("Group %s status changed to %s by %s", group.name, body.status, current_user.username) diff --git a/packages/meshbay-hub/src/meshbay_hub/api/relay.py b/packages/meshbay-hub/src/meshbay_hub/api/relay.py index 08d935b..7bb3f66 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/relay.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/relay.py @@ -102,7 +102,8 @@ async def relay_register( approved = _relays.get(body.relay_id) if not approved or approved.get("pk") != body.pk_relay: raise HTTPException(status_code=403, - detail="Relay not approved — ask hub admin to run POST /v1/relays/approve") + detail="Relay not approved — ask the hub admin to " + "run POST /v1/relays/approve") if body.timestamp is None or not body.signature: raise HTTPException( diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index ee8aabc..2666e86 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -1312,7 +1312,9 @@ async def register_node_key( if len(raw) != 32: raise ValueError except Exception: - raise HTTPException(status_code=400, detail="Invalid Ed25519 public key (need 32 bytes base64)") + raise HTTPException( + status_code=400, + detail="Invalid Ed25519 public key (need 32 bytes base64)") current_user.pk_node_ed25519 = body.pk_node_ed25519 await db.commit() diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py index 3e23961..054d04a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -168,7 +168,8 @@ _HTML = """\ though it had scrolled away. This asks for the keyboard to resize the layout viewport instead, so what is pinned stays where it is looked at. Ignored by browsers that do not know it. --> - + MeshBay diff --git a/packages/meshbay-hub/src/meshbay_hub/csam.py b/packages/meshbay-hub/src/meshbay_hub/csam.py index e540068..b8e8d04 100644 --- a/packages/meshbay-hub/src/meshbay_hub/csam.py +++ b/packages/meshbay-hub/src/meshbay_hub/csam.py @@ -22,6 +22,11 @@ must be reported to NCMEC (US law) or relevant authority immediately. import logging from pathlib import Path +from fastapi import APIRouter, Depends, HTTPException + +from meshbay_hub.api.deps import require_admin +from meshbay_hub.db.models import User + log = logging.getLogger(__name__) # Default path for the CSAM hash database (blake3 hex hashes, one per line) @@ -121,11 +126,6 @@ def check_content_hash(blake3_hex: str) -> bool: # ── Hub API integration ─────────────────────────────────────────────────────── -from fastapi import APIRouter, Depends, HTTPException - -from meshbay_hub.api.deps import require_admin -from meshbay_hub.db.models import User - csam_router = APIRouter(prefix="/v1/admin/csam", tags=["csam"]) diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py index 38c4723..ac1828f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/models.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py @@ -53,10 +53,12 @@ class User(Base): # the node does the wrapping, nothing reads a key from this directory. Keys # are generated per node and pinned there (meshbay_node/roster.py). email_hash: Mapped[str | None] = mapped_column(String(64), nullable=True) # HMAC blind index - pk_node_ed25519: Mapped[str | None] = mapped_column(String(64), nullable=True) # node daemon key + pk_node_ed25519: Mapped[str | None] = mapped_column(String(64), nullable=True) hub_id: Mapped[str] = mapped_column(String(128), nullable=False) - role: Mapped[str] = mapped_column(String(16), default="user") # user|moderator|admin - status: Mapped[str] = mapped_column(String(16), default="active") # active|suspended|revoked + # user|moderator|admin + role: Mapped[str] = mapped_column(String(16), default="user") + # active|suspended|revoked + status: Mapped[str] = mapped_column(String(16), default="active") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) nodes: Mapped[list["Node"]] = relationship(back_populates="user") @@ -105,7 +107,8 @@ class Group(Base): visibility: Mapped[str] = mapped_column(String(16), default="private") # public|private join_policy: Mapped[str] = mapped_column(String(16), default="invite") # open|request|invite description: Mapped[str | None] = mapped_column(String(512)) - status: Mapped[str] = mapped_column(String(16), default="active") # active|suspended|revoked + # active|suspended|revoked + status: Mapped[str] = mapped_column(String(16), default="active") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) # First time a node registered on /v1/nodes/ws announcing that it hosts this # group. Until then the group has no files, no key and nobody to serve it, so @@ -382,7 +385,8 @@ class IPLog(Base): __tablename__ = "ip_logs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id")) # null for failed logins + # null for failed logins + user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id")) # The name this account had, written when it is deleted. The username is # released on deletion and the row itself is tombstoned, so the join that # normally supplies the name would answer "deleted-3f9a1c" for exactly the @@ -390,7 +394,8 @@ class IPLog(Base): username: Mapped[str | None] = mapped_column(String(64)) event: Mapped[str] = mapped_column(String(32), nullable=False) ip_address: Mapped[str] = mapped_column(String(45), nullable=False) # IPv4 or IPv6 - detail: Mapped[str | None] = mapped_column(String(256)) # e.g. username on fail + # e.g. username on fail + detail: Mapped[str | None] = mapped_column(String(256)) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) user: Mapped["User | None"] = relationship(back_populates="ip_logs") diff --git a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py index 429575f..1ef7796 100644 --- a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py +++ b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py @@ -50,7 +50,8 @@ async def cleanup_loop(get_session): async with get_session() as db: deleted = await purge_old_ip_logs(db) if deleted: - log.info("Purged %d IP log entries older than %d days", deleted, RETENTION_DAYS) + log.info("Purged %d IP log entries older than %d days", + deleted, RETENTION_DAYS) expired = await purge_expired_verifications(db) if expired: log.info("Purged %d expired email verifications", expired) diff --git a/packages/meshbay-hub/tests/harness/boot_guard_probe.py b/packages/meshbay-hub/tests/harness/boot_guard_probe.py index 01751ba..b4a5e7c 100644 --- a/packages/meshbay-hub/tests/harness/boot_guard_probe.py +++ b/packages/meshbay-hub/tests/harness/boot_guard_probe.py @@ -83,7 +83,8 @@ const cases = []; const post = (o) => fetch('/log', { method: 'POST', body: JSON.stringify(o) }); addEventListener('error', (e) => post({ error: 'page error: ' + (e.message || e) })); addEventListener('unhandledrejection', - (e) => post({ error: 'rejection: ' + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason) })); + (e) => post({ error: 'rejection: ' + + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason) })); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const add = (src) => { const f = document.createElement('iframe'); diff --git a/packages/meshbay-hub/tests/harness/menu_scroll_probe.py b/packages/meshbay-hub/tests/harness/menu_scroll_probe.py index 5fb9579..c4d3b87 100755 --- a/packages/meshbay-hub/tests/harness/menu_scroll_probe.py +++ b/packages/meshbay-hub/tests/harness/menu_scroll_probe.py @@ -48,7 +48,8 @@ import { Menu } from '/menu.js'; const LOGS = []; addEventListener('error', (e) => LOGS.push('error: ' + (e.message || e))); addEventListener('unhandledrejection', - (e) => LOGS.push('rejection: ' + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); + (e) => LOGS.push('rejection: ' + + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const frame = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); diff --git a/packages/meshbay-hub/tests/harness/music_grid_probe.py b/packages/meshbay-hub/tests/harness/music_grid_probe.py index 365e028..0cf65bc 100644 --- a/packages/meshbay-hub/tests/harness/music_grid_probe.py +++ b/packages/meshbay-hub/tests/harness/music_grid_probe.py @@ -100,7 +100,8 @@ import { MusicPlayerBar } from '/music-player.js'; const LOGS = []; addEventListener('error', (e) => LOGS.push('error: ' + (e.message || e))); addEventListener('unhandledrejection', - (e) => LOGS.push('rejection: ' + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); + (e) => LOGS.push('rejection: ' + + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const waitFor = async (sel, tries = 60) => { @@ -209,7 +210,8 @@ const clickMenu = async (i) => { const last = rows[rows.length - 1]; const entry = { artist: label, top, heading: heading ? heading.textContent : null, - headingH: heading ? Math.round(heading.getBoundingClientRect().height) : null }; + headingH: heading + ? Math.round(heading.getBoundingClientRect().height) : null }; if (last && Math.abs(last.top - top) < 8) last.cells.push(entry); else rows.push({ top, cells: [entry] }); } diff --git a/packages/meshbay-hub/tests/harness/music_queue_probe.py b/packages/meshbay-hub/tests/harness/music_queue_probe.py index a9146ce..9138db9 100755 --- a/packages/meshbay-hub/tests/harness/music_queue_probe.py +++ b/packages/meshbay-hub/tests/harness/music_queue_probe.py @@ -99,7 +99,8 @@ import { MusicPlayerBar } from '/music-player.js'; const LOGS = []; addEventListener('error', (e) => LOGS.push('error: ' + (e.message || e))); addEventListener('unhandledrejection', - (e) => LOGS.push('rejection: ' + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); + (e) => LOGS.push('rejection: ' + + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const waitFor = async (sel, tries = 60) => { diff --git a/packages/meshbay-hub/tests/harness/playlist_store_probe.py b/packages/meshbay-hub/tests/harness/playlist_store_probe.py index 6421426..9a54a0d 100755 --- a/packages/meshbay-hub/tests/harness/playlist_store_probe.py +++ b/packages/meshbay-hub/tests/harness/playlist_store_probe.py @@ -38,7 +38,8 @@ import { MANIFEST_KIND, bodyKind } from '/playlist-merge.js'; const LOGS = []; addEventListener('error', (e) => LOGS.push('error: ' + (e.message || e))); addEventListener('unhandledrejection', - (e) => LOGS.push('rejection: ' + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); + (e) => LOGS.push('rejection: ' + + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); const USER = 'user-1'; const steps = []; @@ -95,7 +96,8 @@ function fakeNode() { await P.addTracks(USER, P.FAVORITES_ID, [track(9)], 'g1', 'Favoris'); steps.push({ step: 'after editing', - list: (await P.listPlaylists(USER)).map((p) => ({ id: p.id, name: p.name, count: p.count })) }); + list: (await P.listPlaylists(USER)) + .map((p) => ({ id: p.id, name: p.name, count: p.count })) }); steps.push({ step: 'tracks read back', tracks: (await P.getPlaylistTracks(USER, eveningId)).map((t) => ({ diff --git a/packages/meshbay-hub/tests/harness/playlist_ui_probe.py b/packages/meshbay-hub/tests/harness/playlist_ui_probe.py index f0fc9c3..b60baf6 100644 --- a/packages/meshbay-hub/tests/harness/playlist_ui_probe.py +++ b/packages/meshbay-hub/tests/harness/playlist_ui_probe.py @@ -98,7 +98,8 @@ import * as P from '/playlists.js'; const LOGS = []; addEventListener('error', (e) => LOGS.push('error: ' + (e.message || e))); addEventListener('unhandledrejection', - (e) => LOGS.push('rejection: ' + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); + (e) => LOGS.push('rejection: ' + + ((e.reason && (e.reason.stack || e.reason.message)) || e.reason))); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const waitFor = async (sel, tries = 60) => { @@ -253,7 +254,8 @@ const clickMenu = async (i) => { open.click(); await waitFor('.music-detail .music-tracklist'); steps.push({ step: 'loaded into the queue', - play: [...document.querySelectorAll('.music-detail .music-tracklist .music-track-title')] + play: [...document.querySelectorAll( + '.music-detail .music-tracklist .music-track-title')] .map((e) => e.textContent) }); document.querySelector('.music-detail .video-close').click(); await sleep(150); @@ -267,8 +269,10 @@ const clickMenu = async (i) => { await clickLabel('A2-t2'); await sleep(300); steps.push({ step: 'track removed', - lists: (await P.listPlaylists('u1')).map((p) => ({ name: p.name, count: p.count })), - tracks: (await P.getPlaylistTracks('u1', lists.find((p) => p.name === 'Soirée').id)) + lists: (await P.listPlaylists('u1')) + .map((p) => ({ name: p.name, count: p.count })), + tracks: (await P.getPlaylistTracks('u1', + lists.find((p) => p.name === 'Soirée').id)) .map((tr) => tr.display_title) }); // 6. Delete it. diff --git a/packages/meshbay-hub/tests/test_files_drop_upload.py b/packages/meshbay-hub/tests/test_files_drop_upload.py index 5dfaf23..fc15c47 100644 --- a/packages/meshbay-hub/tests/test_files_drop_upload.py +++ b/packages/meshbay-hub/tests/test_files_drop_upload.py @@ -66,7 +66,8 @@ def test_names_in_a_folder_count_files_folders_and_empty_folders(tmp_path, sourc {"path": "music/Album", "name": "t.flac"}, {"path": "musicals", "name": "not-here.txt"}] got = _run(tmp_path, source, - f"namesIn({json.dumps(entries)}, ['music/Empty', 'music/Album/cd1'], 'music').sort()") + f"namesIn({json.dumps(entries)}, " + f"['music/Empty', 'music/Album/cd1'], 'music').sort()") assert got == ["Album", "Empty", "a.mp3"] diff --git a/packages/meshbay-hub/tests/test_hook_ordering.py b/packages/meshbay-hub/tests/test_hook_ordering.py index 3c82bb0..d03cdf5 100644 --- a/packages/meshbay-hub/tests/test_hook_ordering.py +++ b/packages/meshbay-hub/tests/test_hook_ordering.py @@ -123,7 +123,8 @@ def test_the_check_would_notice(): # Inject a dependency on `last` into the first declaration's dep array. end = broken.index("\n }, [", decls[0].start()) close = broken.index("]", end) - broken = broken[:close] + (", " if broken[end + 7:close].strip() else "") + last + broken[close:] + broken = (broken[:close] + (", " if broken[end + 7:close].strip() else "") + + last + broken[close:]) declared_at = {m.group(1): m.start() for m in DECL.finditer(broken)} caught = False diff --git a/packages/meshbay-hub/tests/test_hub_api.py b/packages/meshbay-hub/tests/test_hub_api.py index 9b73701..2afd27b 100644 --- a/packages/meshbay-hub/tests/test_hub_api.py +++ b/packages/meshbay-hub/tests/test_hub_api.py @@ -260,7 +260,9 @@ async def test_group_member_add(client): "pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x}) alice_token = (await client.post("/v1/users/login", - json={"username": "alice2_test", "password": "alicepass99"})).json()["access_token"] + json={"username": "alice2_test", + "password": "alicepass99"}) + ).json()["access_token"] a_hdrs = {"Authorization": f"Bearer {alice_token}"} @@ -298,9 +300,13 @@ async def test_non_admin_cannot_add_member(client): "pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x}) charlie_token = (await client.post("/v1/users/login", - json={"username": "charlie_test", "password": "charliepass"})).json()["access_token"] + json={"username": "charlie_test", + "password": "charliepass"}) + ).json()["access_token"] dan_token = (await client.post("/v1/users/login", - json={"username": "dan_test", "password": "danpass1234"})).json()["access_token"] + json={"username": "dan_test", + "password": "danpass1234"}) + ).json()["access_token"] r = await client.post("/v1/groups", json={"name": "charlies-group"}, headers={"Authorization": f"Bearer {charlie_token}"}) @@ -338,7 +344,9 @@ async def test_jwt_contains_groups_claim(client): # Alice creates a group and adds Bob alice_token = (await client.post("/v1/users/login", - json={"username": "grp_alice", "password": "alicepass99"})).json()["access_token"] + json={"username": "grp_alice", + "password": "alicepass99"}) + ).json()["access_token"] r = await client.post("/v1/groups", json={"name": "testgroup"}, headers={"Authorization": f"Bearer {alice_token}"}) group_id = r.json()["group_id"] diff --git a/packages/meshbay-hub/tests/test_layout_measured.py b/packages/meshbay-hub/tests/test_layout_measured.py index a71b6b9..9bf1bde 100644 --- a/packages/meshbay-hub/tests/test_layout_measured.py +++ b/packages/meshbay-hub/tests/test_layout_measured.py @@ -178,7 +178,8 @@ GROUPED = textwrap.dedent("""
-
210 MB / 493 MB3.1 MB/s · 4 min left
+
210 MB / 493 MB + 3.1 MB/s · 4 min left
@@ -190,7 +191,8 @@ GROUPED = textwrap.dedent("""
-
Waiting — your slots are busy1.2 GB
+
Waiting — your slots are busy + 1.2 GB
diff --git a/packages/meshbay-hub/tests/test_locales.py b/packages/meshbay-hub/tests/test_locales.py index 59ea2bf..602d161 100644 --- a/packages/meshbay-hub/tests/test_locales.py +++ b/packages/meshbay-hub/tests/test_locales.py @@ -158,7 +158,8 @@ def test_locale_resolution_is_region_aware(tmp_path): const out = {}; for (const tags of [['pt-BR'], ['pt'], ['zh-CN'], ['zh'], ['fr-CA'], ['de-AT'], ['ru', 'it'], ['ko']]) { - Object.defineProperty(globalThis, 'navigator', { value: { languages: tags, language: tags[0] }, configurable: true }); + Object.defineProperty(globalThis, 'navigator', + { value: { languages: tags, language: tags[0] }, configurable: true }); delete store.mb_lang; out[tags.join(',')] = await i18n.initLocale(); } @@ -186,7 +187,8 @@ def test_counted_string_picks_the_right_polish_form(tmp_path): setItem: (k, v) => { store[k] = v; }, }; globalThis.document = { documentElement: {} }; - Object.defineProperty(globalThis, 'navigator', { value: { languages: ['pl'], language: 'pl' }, configurable: true }); + Object.defineProperty(globalThis, 'navigator', + { value: { languages: ['pl'], language: 'pl' }, configurable: true }); const i18n = await import('./i18n.js'); await i18n.initLocale(); console.log(JSON.stringify( @@ -205,7 +207,8 @@ def test_interpolated_value_is_not_read_as_a_replacement_pattern(tmp_path): setItem: (k, v) => { store[k] = v; }, }; globalThis.document = { documentElement: {} }; - Object.defineProperty(globalThis, 'navigator', { value: { languages: ['en'], language: 'en' }, configurable: true }); + Object.defineProperty(globalThis, 'navigator', + { value: { languages: ['en'], language: 'en' }, configurable: true }); const i18n = await import('./i18n.js'); await i18n.initLocale(); console.log(JSON.stringify( diff --git a/packages/meshbay-hub/tests/test_memory_ceiling.py b/packages/meshbay-hub/tests/test_memory_ceiling.py index 1654825..5984292 100644 --- a/packages/meshbay-hub/tests/test_memory_ceiling.py +++ b/packages/meshbay-hub/tests/test_memory_ceiling.py @@ -246,11 +246,11 @@ def test_no_unguarded_memory_floor(target_fn): # definition out before looking. Comments go too — the branch that used to # be the bug is now described in one, and a test that reads prose is the # mistake already recorded in CLAUDE.md for the packaged systemd unit. - start = next(n for n, l in enumerate(lines) if "const _memoryFloor" in l) + start = next(n for n, ln in enumerate(lines) if "const _memoryFloor" in ln) end = next(n for n in range(start, len(lines)) if lines[n].strip() == "};") rest = lines[:start] + lines[end + 1:] - code = [re.sub(r"//.*$", "", l) for l in rest] - bare = [l.strip() for l in code if re.search(r"\breturn null\b", l)] + code = [re.sub(r"//.*$", "", ln) for ln in rest] + bare = [ln.strip() for ln in code if re.search(r"\breturn null\b", ln)] assert bare == [], ( "an unguarded in-memory fallback was added to _openDownloadTarget; " "return _memoryFloor() instead: " + "; ".join(bare)) diff --git a/packages/meshbay-hub/tests/test_notifications_behaviour.py b/packages/meshbay-hub/tests/test_notifications_behaviour.py index 67fd99e..4684d3f 100644 --- a/packages/meshbay-hub/tests/test_notifications_behaviour.py +++ b/packages/meshbay-hub/tests/test_notifications_behaviour.py @@ -35,7 +35,7 @@ async def test_chat_keeps_one_notification_per_group(client, db_session): """Forty messages are one line saying when the conversation last spoke.""" from meshbay_hub.api.notifications import create_notification - token = await _user(client, "listener") + await _user(client, "listener") owner = await _user(client, "talker_test") g = await client.post("/v1/groups", json={"name": "busy"}, headers={"Authorization": f"Bearer {owner}"}) diff --git a/packages/meshbay-hub/tests/test_transfers.py b/packages/meshbay-hub/tests/test_transfers.py index a776b50..9b65ca5 100644 --- a/packages/meshbay-hub/tests/test_transfers.py +++ b/packages/meshbay-hub/tests/test_transfers.py @@ -243,7 +243,10 @@ class L { acquire() { return this._wait; } release(reason) { if (!this.closed) { this.closed = true; this.released.push(reason); } } grant() { this.state = 'granted'; if (this._onState) this._onState(this); this._go(); } - push(state, ahead) { this.state = state; this.ahead = ahead; if (this._onState) this._onState(this); } + push(state, ahead) { + this.state = state; this.ahead = ahead; + if (this._onState) this._onState(this); + } } """ diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index a0ce211..bb50bcf 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -2243,7 +2243,8 @@ def main() -> None: _GUIDANCE = { "node_key_link": ( "Link node key", - f"Copy the node key above and paste it in Settings → Link Node on {cfg.hub.url}"), + f"Copy the node key above and paste it in " + f"Settings → Link Node on {cfg.hub.url}"), "group_add": ( "Add a group", "meshbay-node group add --dir /path/to/files"), diff --git a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py index 5df70c4..91bf2bd 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py @@ -218,7 +218,8 @@ class ParsedName: year: int | None = None season: int | None = None episode: int | None = None - confidence: bool = False # True only when display_title is set and structurally corroborated + # True only when display_title is set and structurally corroborated + confidence: bool = False def parse_movie_filename(filename: str) -> ParsedName: diff --git a/packages/meshbay-node/src/meshbay_node/revocation.py b/packages/meshbay-node/src/meshbay_node/revocation.py index 1e8caee..dede5d0 100644 --- a/packages/meshbay-node/src/meshbay_node/revocation.py +++ b/packages/meshbay-node/src/meshbay_node/revocation.py @@ -19,7 +19,6 @@ import json import logging from typing import Literal -import httpx import jwt log = logging.getLogger(__name__) @@ -108,13 +107,9 @@ class RevocationSubscriber: ws_url = self._hub_url.replace("http://", "ws://").replace("https://", "wss://") ws_url += "/v1/nodes/ws" - async with httpx.AsyncClient() as client: - async with client.stream("GET", ws_url, - headers={"Upgrade": "websocket"}) as resp: - # Use websockets library for proper WS protocol - pass - - # Use websockets library directly + # An httpx stream was opened to this URL here and immediately dropped — + # one pointless request per connect, left over from before the websockets + # library was used directly. import websockets async with websockets.connect(ws_url) as ws: # Authenticate diff --git a/packages/meshbay-node/src/meshbay_node/transfers.py b/packages/meshbay-node/src/meshbay_node/transfers.py index 86c8d14..3345cb9 100644 --- a/packages/meshbay-node/src/meshbay_node/transfers.py +++ b/packages/meshbay-node/src/meshbay_node/transfers.py @@ -393,7 +393,7 @@ class TransferSlots: # the one place it would be tempting to add one for a prettier # log line. for x in sorted(self.leases.values(), - key=lambda l: (l.kind, l.state, l.created_at)) + key=lambda ls: (ls.kind, ls.state, ls.created_at)) ], } diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 2bd1419..e92ec13 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -3930,7 +3930,8 @@ class WebRTCPeerSession: if not entry: thumb = await self._try_serve_thumbnail(file_id, chunk_index, ctx.get("gek")) if thumb is not None: - log.debug("file_req file_id=%s chunk=%s: served as thumbnail", file_id[:16], chunk_index) + log.debug("file_req file_id=%s chunk=%s: served as thumbnail", + file_id[:16], chunk_index) self._send(thumb) return log.warning("File not found: %s", file_id[:16]) @@ -3998,7 +3999,8 @@ class WebRTCPeerSession: self._leaseless.finish(str(file_id)) @staticmethod - async def _fetch_and_cache_poster(media_cache, tmdb_client, poster_path: str | None) -> str | None: + async def _fetch_and_cache_poster(media_cache, tmdb_client, + poster_path: str | None) -> str | None: """ Downloads a TMDB poster/backdrop once, caches it under its own blake3 like a video thumbnail (docs/MESHBAY_DESIGN.md §9.7), and @@ -4026,7 +4028,8 @@ class WebRTCPeerSession: return thumb_hash @staticmethod - async def _fetch_and_cache_cover(media_cache, musicbrainz_client, mbid: str | None) -> str | None: + async def _fetch_and_cache_cover(media_cache, musicbrainz_client, + mbid: str | None) -> str | None: """ Music app equivalent of `_fetch_and_cache_poster` — a release's Cover Art Archive image, fetched once per mbid and cached under its @@ -4510,7 +4513,9 @@ class WebRTCPeerSession: # itself. if not fetched.get("overview"): fallback = await tmdb_client.tv_season(tmdb_id, season, language="en-US") or {} - fetched = {**fallback, **{k: v for k, v in fetched.items() if v not in (None, "", [])}} + fetched = {**fallback, + **{k: v for k, v in fetched.items() + if v not in (None, "", [])}} await media_cache.set_season_meta(tmdb_id, season, fetched) details = fetched @@ -4839,8 +4844,10 @@ class WebRTCPeerSession: # back to English per field rather than discarding an otherwise-good # localized response over one empty one — mirrored here the same # way, at field granularity, not by abandoning the whole response. - if not details.get("overview") or not details.get("poster_path") or not details.get("genres"): - fallback = (await tmdb_client.tv_details(tmdb_id, language="en-US") if media_type == "tv" + if (not details.get("overview") or not details.get("poster_path") + or not details.get("genres")): + fallback = (await tmdb_client.tv_details(tmdb_id, language="en-US") + if media_type == "tv" else await tmdb_client.movie_details(tmdb_id, language="en-US")) or {} details = {**fallback, **{k: v for k, v in details.items() if v not in (None, "", [])}} credits = (await tmdb_client.tv_credits(tmdb_id) if media_type == "tv" @@ -5915,7 +5922,8 @@ class WebRTCPeerSession: transcript = admin_transcript( op=pending["op"], node_pk_b64=self._node_pk_b64(), - group_id=pending["group_id"] if pending.get("group_id") is not None else (self._group_id or ""), + group_id=(pending["group_id"] if pending.get("group_id") is not None + else (self._group_id or "")), subject=pending["subject"], nonce=pending["nonce"], ts=pending["ts"], diff --git a/packages/meshbay-node/tests/test_audio_transcode.py b/packages/meshbay-node/tests/test_audio_transcode.py index a6557f0..acb39b7 100644 --- a/packages/meshbay-node/tests/test_audio_transcode.py +++ b/packages/meshbay-node/tests/test_audio_transcode.py @@ -145,7 +145,8 @@ async def test_missing_file_id_is_an_error(tmp_path, media_cache): sk_node = Ed25519PrivateKey.generate() session = WebRTCPeerSession.__new__(WebRTCPeerSession) session._ctx = { - "roots": one_root(tmp_path), "index": GroupIndex(group_id="g" * 32, sk_node=sk_node, gek=gek), + "roots": one_root(tmp_path), + "index": GroupIndex(group_id="g" * 32, sk_node=sk_node, gek=gek), "gek": gek, "sk_node": sk_node, "media_cache": media_cache, } session._group_id = None @@ -170,7 +171,8 @@ async def test_multi_chunk_cached_blob_reassembles_correctly(tmp_path, media_cac sk_node = Ed25519PrivateKey.generate() session = WebRTCPeerSession.__new__(WebRTCPeerSession) session._ctx = { - "roots": one_root(tmp_path), "index": GroupIndex(group_id="g" * 32, sk_node=sk_node, gek=gek), + "roots": one_root(tmp_path), + "index": GroupIndex(group_id="g" * 32, sk_node=sk_node, gek=gek), "gek": gek, "sk_node": sk_node, "media_cache": media_cache, } session._group_id = None diff --git a/packages/meshbay-node/tests/test_enrich_photo.py b/packages/meshbay-node/tests/test_enrich_photo.py index fb0530c..d1bbd28 100644 --- a/packages/meshbay-node/tests/test_enrich_photo.py +++ b/packages/meshbay-node/tests/test_enrich_photo.py @@ -166,7 +166,8 @@ async def test_enricher_corrects_orientation(tmp_path, media_cache): "width/height must reflect the EXIF-corrected orientation, not the raw stored frame") thumb_bytes = await media_cache.get_thumb(fields["thumb_hash"]) thumb = Image.open(io.BytesIO(thumb_bytes)) - assert thumb.size[0] < thumb.size[1], "the stored thumbnail itself must be portrait, not sideways" + assert thumb.size[0] < thumb.size[1], ( + "the stored thumbnail itself must be portrait, not sideways") def test_gps_is_never_read_by_this_module(): diff --git a/packages/meshbay-node/tests/test_indexer.py b/packages/meshbay-node/tests/test_indexer.py index 48bf399..e97e2ef 100644 --- a/packages/meshbay-node/tests/test_indexer.py +++ b/packages/meshbay-node/tests/test_indexer.py @@ -396,7 +396,8 @@ async def test_scan_interrupted_partway_leaves_only_completed_files_cached( indexer_mod._scan_file = real_scan_file assert resumed.index.count == 5 - assert len(calls) == 2, f"expected only the 2 not-yet-cached files to be hashed, got {len(calls)}" + assert len(calls) == 2, ( + f"expected only the 2 not-yet-cached files to be hashed, got {len(calls)}") # ── Progress state ─────────────────────────────────────────────────────────── diff --git a/packages/meshbay-node/tests/test_multi_group.py b/packages/meshbay-node/tests/test_multi_group.py index e095426..b75bad0 100644 --- a/packages/meshbay-node/tests/test_multi_group.py +++ b/packages/meshbay-node/tests/test_multi_group.py @@ -72,10 +72,12 @@ async def multi_group_server(sk_node, sk_hub, gek_a, gek_b, dir_a, dir_b, tmp_pa hub_pk_pem = sk_hub.public_key().public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo) - indexer_a = DirectoryIndexer(roots=one_root(dir_a), group_id="group-a", sk_node=sk_node, gek=gek_a) + indexer_a = DirectoryIndexer(roots=one_root(dir_a), group_id="group-a", + sk_node=sk_node, gek=gek_a) await indexer_a.initial_scan() - indexer_b = DirectoryIndexer(roots=one_root(dir_b), group_id="group-b", sk_node=sk_node, gek=gek_b) + indexer_b = DirectoryIndexer(roots=one_root(dir_b), group_id="group-b", + sk_node=sk_node, gek=gek_b) await indexer_b.initial_scan() # RootSet, not a bare Path — what the daemon actually puts in a group context. diff --git a/packages/meshbay-node/tests/test_node_status.py b/packages/meshbay-node/tests/test_node_status.py index 7a27623..8bb2f39 100644 --- a/packages/meshbay-node/tests/test_node_status.py +++ b/packages/meshbay-node/tests/test_node_status.py @@ -207,7 +207,6 @@ async def test_node_status_catches_send_failure(tmp_path, roster): not silently vanish — it used to, because _send was outside the try block.""" session = await _session(tmp_path, roster, operator=True, node_user_id="grenet") - original_send = session._send sent = [] call_count = [0] diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py index 7b44bbe..aa77c29 100644 --- a/packages/meshbay-node/tests/test_packaging_win.py +++ b/packages/meshbay-node/tests/test_packaging_win.py @@ -1230,13 +1230,14 @@ def test_service_mode_ps1_starts_the_task_after_installing_it(): already-running task pointlessly on every mode switch away from service. """ src = (WIN / "service-mode.ps1").read_text(encoding="utf-8") - install_branch, remove_branch = src.split('$Action -eq "install"', 1)[1], None + install_branch = src.split('$Action -eq "install"', 1)[1] assert '"run"' in install_branch or "'run'" in install_branch, ( "service-mode.ps1 registers the task but never starts it -- the " "daemon stays down until the next reboot") # The run step must be conditioned on install having actually succeeded, # not fired unconditionally regardless of $Action. - guard_line = src[src.index('$Action -eq "install"') - 40:src.index('$Action -eq "install"') + 40] + at = src.index('$Action -eq "install"') + guard_line = src[at - 40:at + 40] assert "-and" in guard_line or "-not $failed" in install_branch, ( "the follow-up `run` must be gated on Action=install and success, " "not run unconditionally on every invocation including remove") diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py index 67c3b08..82336e2 100644 --- a/packages/meshbay-node/tests/test_roster_pairing.py +++ b/packages/meshbay-node/tests/test_roster_pairing.py @@ -569,7 +569,8 @@ def test_challenge_carries_node_pk_in_source(): builds, whatever the surrounding handshake does. """ source = (Path(__file__).parent.parent - / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text(encoding="utf-8") + / "src" / "meshbay_node" / "transport" + / "webrtc_server.py").read_text(encoding="utf-8") challenge = source[source.find("MNP.HANDSHAKE_CHALLENGE,"):] challenge = challenge[:challenge.find("})")] assert "node_pk" in challenge, ( diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py index bfe25b9..7010523 100644 --- a/packages/meshbay-node/tests/test_security_regressions.py +++ b/packages/meshbay-node/tests/test_security_regressions.py @@ -462,7 +462,8 @@ def test_no_member_can_hand_the_node_key_material(tmp_path): ) source = (Path(__file__).parent.parent - / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text(encoding="utf-8") + / "src" / "meshbay_node" / "transport" + / "webrtc_server.py").read_text(encoding="utf-8") assert "_do_gek_bundle_store" not in source assert "_admin_exec_bundle_store" not in source diff --git a/packages/meshbay-node/tests/test_transfer_slots_wire.py b/packages/meshbay-node/tests/test_transfer_slots_wire.py index c9fc85a..d96456c 100644 --- a/packages/meshbay-node/tests/test_transfer_slots_wire.py +++ b/packages/meshbay-node/tests/test_transfer_slots_wire.py @@ -19,6 +19,9 @@ Three things can only be checked here: """ import pytest +from meshbay_common.protocol import MNP +from meshbay_node.transfers import DOWNLOAD, UPLOAD +from meshbay_node.transport.webrtc_server import WebRTCPeerSession # Every test drives a message handler, and in the node a message handler always # runs inside the event loop: `_do_transfer_open` starts the sweeper task there. @@ -26,10 +29,6 @@ import pytest # on "no current event loop" the moment the sweeper stopped being faked. pytestmark = pytest.mark.asyncio -from meshbay_common.protocol import MNP -from meshbay_node.transfers import DOWNLOAD, UPLOAD -from meshbay_node.transport.webrtc_server import WebRTCPeerSession - class _Session(WebRTCPeerSession): """A session with the DataChannel replaced by a list, and nothing else.""" diff --git a/packages/meshbay-node/tests/test_webrtc_transport.py b/packages/meshbay-node/tests/test_webrtc_transport.py index 4aba99f..91a6e1c 100644 --- a/packages/meshbay-node/tests/test_webrtc_transport.py +++ b/packages/meshbay-node/tests/test_webrtc_transport.py @@ -26,6 +26,11 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PublicKey, ) from meshbay_common import MNP_VERSION +from meshbay_common.adminop import ( + OP_FILE_DELETE, + OP_INVITE_CREATE, + admin_transcript, +) from meshbay_common.crypto import ( generate_gek, pk_to_b64, @@ -35,16 +40,6 @@ from meshbay_common.crypto import ( wrap_gek_aes, ) from meshbay_common.groupbox import PURPOSE_ACK, PURPOSE_INDEX, unseal -from meshbay_common.protocol import MNP -from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes - -TEST_GROUP = "g" - -from meshbay_common.adminop import ( - OP_FILE_DELETE, - OP_INVITE_CREATE, - admin_transcript, -) from meshbay_common.handshake import ( NONCE_LEN, ROLE_CLIENT, @@ -55,6 +50,8 @@ from meshbay_common.handshake import ( webrtc_binding, ) from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR, join_transcript +from meshbay_common.protocol import MNP +from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node.bundle_store import BundleStore from meshbay_node.indexer import DirectoryIndexer from meshbay_node.roster import Roster @@ -62,6 +59,8 @@ from meshbay_node.transport.webrtc_server import WebRTCTransport from conftest import one_root +TEST_GROUP = "g" + @pytest.fixture def sk_node(): diff --git a/poc/spike1_crypto.py b/poc/spike1_crypto.py index 1e8491c..4006506 100644 --- a/poc/spike1_crypto.py +++ b/poc/spike1_crypto.py @@ -243,7 +243,8 @@ print("\n=== Test 7: AES-256-GCM — Keystore encryption ===") def test_aes_gcm_keystore(): # Derive an AES key from Argon2id (as done for keystore unlock) salt = os.urandom(16) - aes_key = Argon2id(salt=salt, length=32, iterations=3, lanes=4, memory_cost=65536).derive(b"password") + aes_key = Argon2id(salt=salt, length=32, iterations=3, lanes=4, + memory_cost=65536).derive(b"password") # Encrypt a mock keystore blob keystore_data = b'{"sk_user": "base64...", "sk_group": "base64..."}' diff --git a/poc/spike3_node.py b/poc/spike3_node.py index 0b6d694..562239f 100644 --- a/poc/spike3_node.py +++ b/poc/spike3_node.py @@ -27,7 +27,8 @@ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey HUB_URL = "http://meshbay.org" STATE_FILE = Path("node_state.json") # persists keys and tokens between runs -PASS = "✓"; FAIL = "✗" +PASS = "✓" +FAIL = "✗" # ── Key helpers ─────────────────────────────────────────────────────────────── diff --git a/poc/spike4_nat.py b/poc/spike4_nat.py index e34a4c4..ee7485f 100644 --- a/poc/spike4_nat.py +++ b/poc/spike4_nat.py @@ -21,7 +21,9 @@ from pathlib import Path import httpx -PASS = "✓"; FAIL = "✗"; SKIP = "–" +PASS = "✓" +FAIL = "✗" +SKIP = "–" LOCAL_PORT = 19000 MESHBAY_IP = "164.132.246.44" # meshbay.org resolved @@ -229,7 +231,8 @@ async def main(): # May differ from STUN if symmetric NAT actual_ext = seen_ext_addr.replace("('", "").replace("'", "").replace(", ", ":") if endpoint_hint and actual_ext != endpoint_hint: - print(f" ⚠ STUN addr {endpoint_hint} ≠ actual {actual_ext} (symmetric NAT confirmed)") + print(f" ⚠ STUN addr {endpoint_hint} ≠ actual {actual_ext} " + f"(symmetric NAT confirmed)") endpoint_hint = actual_ext else: print(f" {FAIL} No echo received (timeout)") diff --git a/poc/spike5_client.py b/poc/spike5_client.py index 6ac2fba..cc79ebf 100644 --- a/poc/spike5_client.py +++ b/poc/spike5_client.py @@ -27,7 +27,8 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF PORT = 19003 CHUNK_INDEX = 0 -PASS = "✓"; FAIL = "✗" +PASS = "✓" +FAIL = "✗" # ── Wire helpers ─────────────────────────────────────────────────────────────── diff --git a/poc/spike5_node.py b/poc/spike5_node.py index b560dc1..cded922 100644 --- a/poc/spike5_node.py +++ b/poc/spike5_node.py @@ -33,7 +33,8 @@ MESHBAY_PORT = 19003 CHUNK_SIZE = 1024 * 1024 # 1 MB TEST_FILE = Path("testfile.bin") STATE_FILE = Path("node_state.json") -PASS = "✓"; FAIL = "✗" +PASS = "✓" +FAIL = "✗" # ── Wire helpers ─────────────────────────────────────────────────────────────── diff --git a/poc/spike6_gek.py b/poc/spike6_gek.py index 3595d6d..e58823b 100644 --- a/poc/spike6_gek.py +++ b/poc/spike6_gek.py @@ -33,7 +33,8 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF HUB_URL = "http://meshbay.org" STATE_FILE = Path("node_state.json") -PASS = "✓"; FAIL = "✗" +PASS = "✓" +FAIL = "✗" # ── Key helpers ──────────────────────────────────────────────────────────────── @@ -200,7 +201,9 @@ async def main(): bundle_alice = wrap_gek(gek_raw, pk_alice_x_raw) print(f" {PASS} Wrapped in {(time.perf_counter()-t0)*1000:.2f}ms") print(f" pk_eph : {bundle_alice['pk_eph_b64'][:24]}...") - print(f" wrapped : {bundle_alice['wrapped_b64'][:24]}... ({len(base64.b64decode(bundle_alice['wrapped_b64']))}B)") + wrapped = bundle_alice["wrapped_b64"] + print(f" wrapped : {wrapped[:24]}... " + f"({len(base64.b64decode(wrapped))}B)") r = await c.post( f"{HUB_URL}/v1/groups/{group_id}/members/{state.get('username','node_cbesson')}/gek", -- cgit v1.2.3