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-node/src/meshbay_node/config.py | 4 +- packages/meshbay-node/src/meshbay_node/daemon.py | 39 ++++---- .../meshbay-node/src/meshbay_node/hub_client.py | 2 +- .../src/meshbay_node/indexer/__init__.py | 4 +- .../src/meshbay_node/indexer/enrich.py | 6 +- .../src/meshbay_node/indexer/enrich_photo.py | 4 +- .../src/meshbay_node/indexer/group_index.py | 19 ++-- .../src/meshbay_node/indexer/indexer.py | 11 ++- .../src/meshbay_node/indexer/title_parse.py | 2 +- packages/meshbay-node/src/meshbay_node/keystore.py | 6 +- packages/meshbay-node/src/meshbay_node/ops.py | 10 +- .../meshbay-node/src/meshbay_node/replication.py | 2 - .../meshbay-node/src/meshbay_node/revocation.py | 3 +- packages/meshbay-node/src/meshbay_node/roots.py | 4 +- packages/meshbay-node/src/meshbay_node/roster.py | 12 +-- .../src/meshbay_node/transport/__init__.py | 4 +- .../src/meshbay_node/transport/quic_client.py | 7 +- .../src/meshbay_node/transport/quic_server.py | 11 +-- .../src/meshbay_node/transport/tls_cert.py | 9 +- .../src/meshbay_node/transport/webrtc_server.py | 110 +++++++++++---------- packages/meshbay-node/src/meshbay_node/ui/app.py | 2 +- packages/meshbay-node/src/meshbay_node/uploads.py | 2 +- packages/meshbay-node/tests/test_admin_ops_mnp.py | 5 +- .../meshbay-node/tests/test_app_directories.py | 1 - .../tests/test_app_directories_signed.py | 1 - .../meshbay-node/tests/test_apps_enabled_policy.py | 3 +- .../tests/test_audio_root_gates_enrichment.py | 3 +- .../meshbay-node/tests/test_chat_is_bounded.py | 1 - .../meshbay-node/tests/test_chat_multidevice.py | 3 +- .../meshbay-node/tests/test_chat_pagination.py | 2 - packages/meshbay-node/tests/test_chat_store.py | 3 +- packages/meshbay-node/tests/test_cli_dispatch.py | 3 - packages/meshbay-node/tests/test_daemon.py | 13 +-- packages/meshbay-node/tests/test_device_linking.py | 4 +- packages/meshbay-node/tests/test_enrich.py | 8 +- packages/meshbay-node/tests/test_enrich_photo.py | 3 +- packages/meshbay-node/tests/test_group_roster.py | 4 +- .../tests/test_hot_reload_survives_client_close.py | 10 +- packages/meshbay-node/tests/test_hub_client.py | 13 +-- .../meshbay-node/tests/test_hub_ws_group_claim.py | 1 - packages/meshbay-node/tests/test_ice_filter.py | 2 +- packages/meshbay-node/tests/test_index_cache.py | 1 - .../tests/test_index_delta_carries_roots.py | 2 - .../meshbay-node/tests/test_index_no_cleartext.py | 3 +- packages/meshbay-node/tests/test_index_progress.py | 7 +- packages/meshbay-node/tests/test_indexer.py | 17 ++-- packages/meshbay-node/tests/test_keystore.py | 6 +- .../meshbay-node/tests/test_leaseless_reads.py | 5 +- packages/meshbay-node/tests/test_media_cache.py | 3 +- .../tests/test_media_cache_eviction.py | 1 - packages/meshbay-node/tests/test_multi_group.py | 11 +-- .../tests/test_musicbrainz_enabled_policy.py | 3 +- packages/meshbay-node/tests/test_node_status.py | 22 ++--- .../meshbay-node/tests/test_partial_uploads.py | 20 ++-- .../meshbay-node/tests/test_peer_session_limits.py | 6 +- packages/meshbay-node/tests/test_platform.py | 2 +- packages/meshbay-node/tests/test_poster_cache.py | 1 - packages/meshbay-node/tests/test_quic_transport.py | 10 +- .../meshbay-node/tests/test_rename_reenrichment.py | 3 +- .../tests/test_replug_restores_enrichment.py | 4 +- .../meshbay-node/tests/test_root_availability.py | 2 - packages/meshbay-node/tests/test_root_eject.py | 1 - .../tests/test_root_ops_reach_the_live_set.py | 1 - .../tests/test_root_paths_are_operator_only.py | 1 - .../tests/test_root_writable_policy.py | 6 +- packages/meshbay-node/tests/test_roots.py | 12 ++- packages/meshbay-node/tests/test_roster_pairing.py | 4 +- .../tests/test_scan_settings_policy.py | 1 - .../tests/test_season_and_search_requests.py | 1 - .../tests/test_security_regressions.py | 30 +++--- .../tests/test_startup_scan_enrichment.py | 5 +- .../tests/test_stream_audio_track_selection.py | 1 - .../tests/test_stream_audio_transcode.py | 3 +- .../meshbay-node/tests/test_stream_capacity.py | 9 +- .../tests/test_stream_capacity_config.py | 1 - .../tests/test_stream_seek_audio_alignment.py | 1 - .../tests/test_stream_seek_reports_the_keyframe.py | 1 - .../tests/test_stream_subtitle_tracks.py | 1 - .../tests/test_stream_video_transcode.py | 1 - packages/meshbay-node/tests/test_title_parse.py | 1 - packages/meshbay-node/tests/test_tmdb.py | 1 - .../meshbay-node/tests/test_tmdb_config_policy.py | 3 +- .../meshbay-node/tests/test_tmdb_enabled_policy.py | 3 +- .../tests/test_tmdb_language_fallback.py | 1 - .../tests/test_tmdb_override_policy.py | 3 +- .../meshbay-node/tests/test_tmdb_rematch_policy.py | 3 +- .../meshbay-node/tests/test_tmdb_show_director.py | 1 - .../meshbay-node/tests/test_transfer_settings.py | 6 +- packages/meshbay-node/tests/test_transfer_slots.py | 14 ++- .../tests/test_transport_wire_parity.py | 3 +- .../meshbay-node/tests/test_upload_size_cap.py | 1 - .../tests/test_video_root_gates_enrichment.py | 3 +- .../meshbay-node/tests/test_webrtc_transport.py | 33 ++++--- .../meshbay-node/tests/test_windows_root_shapes.py | 1 - .../tests/test_wizard_apps_endpoint.py | 4 +- packages/meshbay-node/tests/transfer_probe.py | 12 +-- 96 files changed, 310 insertions(+), 348 deletions(-) (limited to 'packages/meshbay-node') diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py index 2ae3c7c..437f629 100644 --- a/packages/meshbay-node/src/meshbay_node/config.py +++ b/packages/meshbay-node/src/meshbay_node/config.py @@ -14,9 +14,9 @@ from pathlib import Path from meshbay_node.platform import config_dir, data_dir try: - import tomllib # Python 3.11+ + import tomllib # Python 3.11+ except ImportError: - import tomli as tomllib # type: ignore[no-redef] + import tomli as tomllib # type: ignore[no-redef] log = logging.getLogger(__name__) diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 3492d03..a0ce211 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -25,43 +25,43 @@ Usage: """ import asyncio -from dataclasses import asdict, replace import base64 -import json import logging import os import signal import sys import time +from dataclasses import asdict, replace from pathlib import Path import uvicorn - +from meshbay_common import MNP_VERSION from meshbay_common.background import spawn from meshbay_common.paths import fold -from meshbay_common import MNP_VERSION from meshbay_common.protocol import MNP -from meshbay_node.audit import RETENTION_DAYS as AUDIT_RETENTION_DAYS, AuditStore + +from meshbay_node import uploads as uploads_mod +from meshbay_node.audit import RETENTION_DAYS as AUDIT_RETENTION_DAYS +from meshbay_node.audit import AuditStore from meshbay_node.bundle_store import BundleStore from meshbay_node.chat.store import ChatStore -from meshbay_node.config import Config, DEFAULT_CONFIG_PATH, load_config, write_example_config -from meshbay_node.roots import RootSet, RootError, entry_abs_path, off_disk +from meshbay_node.config import DEFAULT_CONFIG_PATH, Config, load_config from meshbay_node.hub_client import HubClient, HubConfig -from meshbay_node.indexer import DirectoryIndexer, IndexCache, GroupIndex +from meshbay_node.indexer import DirectoryIndexer, GroupIndex, IndexCache from meshbay_node.indexer.enrich import Enricher from meshbay_node.indexer.enrich_audio import AudioEnricher from meshbay_node.indexer.enrich_photo import PhotoEnricher +from meshbay_node.keystore import create_keystore, load_keystore, load_or_create_keystore from meshbay_node.media_cache import MediaCache -from meshbay_node.tmdb import TmdbClient -from meshbay_node import uploads as uploads_mod from meshbay_node.musicbrainz import MusicBrainzClient -from meshbay_node.keystore import create_keystore, load_keystore, load_or_create_keystore from meshbay_node.platform import chmod_private, config_dir, data_dir, state_dir +from meshbay_node.roots import RootError, RootSet, entry_abs_path, off_disk from meshbay_node.roster import Roster +from meshbay_node.tmdb import TmdbClient from meshbay_node.transport import ( - Denylist, QUIC_AVAILABLE, WEBRTC_AVAILABLE, + Denylist, ) from meshbay_node.transport.wire import index_delta_message, index_sync_message @@ -109,8 +109,8 @@ def _owning_directory(path: str, directories: list[str]) -> str | None: def calibrate_argon2(target_ms: int = 500) -> None: """Benchmark Argon2id and suggest parameters targeting ~target_ms.""" - import time import os + import time print(f"Calibrating Argon2id (target: {target_ms}ms) ...") salt = os.urandom(16) @@ -1938,8 +1938,7 @@ def _systemctl_user(verb: str, unit: str, *, not_running_hint: str, def main() -> None: import argparse - from meshbay_node.platform import (configure_event_loop, force_utf8_stdio, - load_node_env) + from meshbay_node.platform import configure_event_loop, force_utf8_stdio, load_node_env force_utf8_stdio() configure_event_loop() # Before anything reads the environment. On Linux systemd has usually loaded @@ -2164,10 +2163,10 @@ def main() -> None: print("Aborted.") return - import subprocess as _sp import json as _json - import urllib.request + import subprocess as _sp import urllib.error + import urllib.request token_file = data_dir_ / "ui-token" if token_file.exists(): @@ -2475,7 +2474,11 @@ def main() -> None: if args.command == "restart-daemon": if sys.platform == "win32": from meshbay_node.platform import ( - autostart_end, autostart_run, service_end, service_run, service_status, + autostart_end, + autostart_run, + service_end, + service_run, + service_status, ) if service_status()["installed"]: service_end() diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py index ef12bb4..58bf657 100644 --- a/packages/meshbay-node/src/meshbay_node/hub_client.py +++ b/packages/meshbay-node/src/meshbay_node/hub_client.py @@ -20,7 +20,7 @@ import logging import time from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable +from typing import Any import httpx import jwt diff --git a/packages/meshbay-node/src/meshbay_node/indexer/__init__.py b/packages/meshbay-node/src/meshbay_node/indexer/__init__.py index c92c730..a40e9f2 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/__init__.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/__init__.py @@ -1,6 +1,6 @@ """Directory indexer and Mesh Group Index.""" -from .indexer import DirectoryIndexer -from .group_index import GroupIndex from .cache import IndexCache +from .group_index import GroupIndex +from .indexer import DirectoryIndexer __all__ = ["DirectoryIndexer", "GroupIndex", "IndexCache"] diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py index b44a246..9881298 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py @@ -13,12 +13,12 @@ the rest asynchronously and hands the result back via a callback. import asyncio import logging +from collections.abc import Awaitable, Callable from pathlib import Path -from typing import Awaitable, Callable import blake3 - from meshbay_common.protocol import IndexEntry + from meshbay_node.indexer import title_parse from meshbay_node.indexer.indexer import MEDIA_EXTENSIONS from meshbay_node.media_cache import MediaCache @@ -173,7 +173,7 @@ async def _make_thumbnail(file_path: Path, duration: float | None) -> bytes | No ) try: stdout, _ = await asyncio.wait_for(proc.communicate(), THUMB_TIMEOUT_SECS) - except asyncio.TimeoutError: + except TimeoutError: proc.kill() await proc.wait() return None diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py index 6613f85..5cacef9 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py @@ -22,13 +22,13 @@ import asyncio import datetime import io import logging +from collections.abc import Awaitable, Callable from pathlib import Path -from typing import Awaitable, Callable import blake3 +from meshbay_common.protocol import IndexEntry from PIL import ExifTags, Image, ImageOps -from meshbay_common.protocol import IndexEntry from meshbay_node.media_cache import MediaCache log = logging.getLogger(__name__) diff --git a/packages/meshbay-node/src/meshbay_node/indexer/group_index.py b/packages/meshbay-node/src/meshbay_node/indexer/group_index.py index 2340c78..9e4e780 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/group_index.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/group_index.py @@ -13,29 +13,27 @@ Delta format: import base64 import logging -import os -import time -from dataclasses import dataclass, field, asdict -from pathlib import Path -from typing import Iterator +from dataclasses import asdict, dataclass, field import blake3 import msgpack import zstandard as zstd from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import ( + pk_to_b64, sign_chunk, verify_chunk_signature, - pk_to_b64, - generate_gek, ) +from meshbay_common.protocol import IndexDelta, IndexEntry from meshbay_common.webcrypto import ( chunk_key_aes as derive_chunk_key, - encrypt_chunk_aes as encrypt_chunk, +) +from meshbay_common.webcrypto import ( decrypt_chunk_aes as decrypt_chunk, ) -from meshbay_common.protocol import IndexEntry, IndexDelta +from meshbay_common.webcrypto import ( + encrypt_chunk_aes as encrypt_chunk, +) log = logging.getLogger(__name__) @@ -171,7 +169,6 @@ class GroupIndex: ) -> "GroupIndex": """Deserialize, verify signature, and decrypt (if private).""" from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - from meshbay_common.crypto import verify_chunk_signature envelope = msgpack.unpackb(data, raw=False) diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py index 3fe4f3e..4b619d7 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py @@ -25,19 +25,20 @@ it is the only thing that recovers a missed event. import asyncio import logging import time +from collections.abc import Awaitable, Callable from concurrent.futures import ThreadPoolExecutor -from dataclasses import dataclass, field as dataclass_field +from dataclasses import dataclass +from dataclasses import field as dataclass_field from pathlib import Path -from typing import Callable, Awaitable import blake3 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from meshbay_common.background import spawn +from meshbay_common.paths import find_fold_collisions, fold, long_path +from meshbay_common.protocol import IndexEntry from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer -from meshbay_common.background import spawn -from meshbay_common.paths import fold, find_fold_collisions, long_path -from meshbay_common.protocol import IndexEntry from meshbay_node.indexer.cache import IndexCache from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import Root, RootSet, off_disk 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 14728ff..5df70c4 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py @@ -19,7 +19,7 @@ directory listing; this module only parses strings it's handed. from __future__ import annotations import re -from dataclasses import dataclass, field +from dataclasses import dataclass from guessit import guessit diff --git a/packages/meshbay-node/src/meshbay_node/keystore.py b/packages/meshbay-node/src/meshbay_node/keystore.py index 00e504e..78fe122 100644 --- a/packages/meshbay-node/src/meshbay_node/keystore.py +++ b/packages/meshbay-node/src/meshbay_node/keystore.py @@ -38,8 +38,6 @@ from pathlib import Path import msgpack from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - -from meshbay_node.platform import chmod_private, config_dir from meshbay_common.crypto import ( ARGON2_ITERATIONS, ARGON2_LANES, @@ -50,12 +48,12 @@ from meshbay_common.crypto import ( decrypt_keystore, derive_keystore_key, encrypt_keystore, - generate_gek, pk_to_b64, sk_to_b64, - sk_to_raw, ) +from meshbay_node.platform import chmod_private, config_dir + log = logging.getLogger(__name__) KEYSTORE_VERSION = 1 diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py index ca1b87d..baea365 100644 --- a/packages/meshbay-node/src/meshbay_node/ops.py +++ b/packages/meshbay-node/src/meshbay_node/ops.py @@ -24,8 +24,8 @@ from __future__ import annotations import asyncio import logging -import time as _time import re +import time as _time from dataclasses import asdict from pathlib import Path from typing import Any @@ -37,8 +37,9 @@ from meshbay_common.crypto import ( unwrap_gek_aes, wrap_gek_aes, ) -from meshbay_node.config import DEFAULT_CONFIG_PATH from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR + +from meshbay_node.config import DEFAULT_CONFIG_PATH from meshbay_node.roots import RootError, RootSet, off_disk from meshbay_node.roster import Roster @@ -951,6 +952,7 @@ async def remove_root(state: dict, group_id: str, root_name: str) -> dict: raise OpError("Group not configured on this node", status=404) from meshbay_common.paths import fold + from meshbay_node.roots import derive_name target = fold(root_name) match_idx = None @@ -1001,6 +1003,7 @@ async def update_root(state: dict, group_id: str, root_name: str, *, raise OpError("Group not configured on this node", status=404) from meshbay_common.paths import fold + from meshbay_node.roots import RootSet target = fold(root_name) match = None @@ -1462,8 +1465,7 @@ async def list_transfers(state: dict) -> dict: ctx = getattr(webrtc, "_ctx", {}) if webrtc else {} slots = ctx.get("_transfer_slots") if slots is None: - from meshbay_node.transfers import ( - DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_PER_MEMBER, KINDS) + from meshbay_node.transfers import DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_PER_MEMBER, KINDS # No pool built means nothing has transferred since the daemon started, # which is a real answer and not an error. # diff --git a/packages/meshbay-node/src/meshbay_node/replication.py b/packages/meshbay-node/src/meshbay_node/replication.py index c2f5cd3..297ed0b 100644 --- a/packages/meshbay-node/src/meshbay_node/replication.py +++ b/packages/meshbay-node/src/meshbay_node/replication.py @@ -16,8 +16,6 @@ Usage: await replicator.replicate_file(file_id, file_name, file_size) """ -import asyncio -import hashlib import logging from pathlib import Path diff --git a/packages/meshbay-node/src/meshbay_node/revocation.py b/packages/meshbay-node/src/meshbay_node/revocation.py index d3ee18f..1e8caee 100644 --- a/packages/meshbay-node/src/meshbay_node/revocation.py +++ b/packages/meshbay-node/src/meshbay_node/revocation.py @@ -17,7 +17,6 @@ Usage in daemon: import asyncio import json import logging -import time from typing import Literal import httpx @@ -151,5 +150,5 @@ class RevocationSubscriber: else: log.debug("WS message: %s", msg.get("type")) - except asyncio.TimeoutError: + except TimeoutError: continue diff --git a/packages/meshbay-node/src/meshbay_node/roots.py b/packages/meshbay-node/src/meshbay_node/roots.py index 67778ca..89b0441 100644 --- a/packages/meshbay-node/src/meshbay_node/roots.py +++ b/packages/meshbay-node/src/meshbay_node/roots.py @@ -71,7 +71,7 @@ def _free_name(directory: Path, filename: str) -> str: raise FileExistsError(filename) -def safe_subdir(roots: "RootSet", rel: str) -> Path | None: +def safe_subdir(roots: RootSet, rel: str) -> Path | None: """ Resolve a client-supplied directory inside one of the group's roots, or refuse. @@ -219,7 +219,7 @@ class RootSet: # ── Construction ───────────────────────────────────────────────────────── @classmethod - def build(cls, specs: list[dict]) -> "RootSet": + def build(cls, specs: list[dict]) -> RootSet: """ Build from configuration, refusing anything ambiguous. diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py index 8c9b3ef..9478a32 100644 --- a/packages/meshbay-node/src/meshbay_node/roster.py +++ b/packages/meshbay-node/src/meshbay_node/roster.py @@ -24,13 +24,11 @@ from __future__ import annotations import hashlib import json import logging -import os import secrets -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path import aiosqlite - from meshbay_common.paths import fold log = logging.getLogger(__name__) @@ -194,11 +192,11 @@ def hash_code(code: str) -> str: def _now() -> str: - return datetime.now(timezone.utc).isoformat(timespec="seconds") + return datetime.now(UTC).isoformat(timespec="seconds") def _iso_in(seconds: int) -> str: - return (datetime.now(timezone.utc) + return (datetime.now(UTC) + timedelta(seconds=seconds)).isoformat(timespec="seconds") @@ -1073,7 +1071,7 @@ class Roster: (group_id, user_id), ) code = generate_code() - expires = datetime.now(timezone.utc) + timedelta(seconds=ttl) + expires = datetime.now(UTC) + timedelta(seconds=ttl) await self._db.execute( "INSERT INTO invites (code_hash, group_id, user_id, username, role, " "created_by, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", @@ -1106,7 +1104,7 @@ class Roster: # redeemed by whoever finds it first. if invite["user_id"] != user_id: return None - if datetime.fromisoformat(invite["expires_at"]) < datetime.now(timezone.utc): + if datetime.fromisoformat(invite["expires_at"]) < datetime.now(UTC): return None cur = await self._db.execute( diff --git a/packages/meshbay-node/src/meshbay_node/transport/__init__.py b/packages/meshbay-node/src/meshbay_node/transport/__init__.py index e423e35..86dbf23 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/__init__.py +++ b/packages/meshbay-node/src/meshbay_node/transport/__init__.py @@ -15,8 +15,8 @@ Transport decision (2026-08-13, second security review): # QUIC transport (MNP v2) — requires aioquic>=1.0 try: - from .quic_server import QuicChunkServer, Denylist from .quic_client import QuicChunkClient + from .quic_server import Denylist, QuicChunkServer QUIC_AVAILABLE = True except ImportError: QuicChunkServer = None # type: ignore[assignment,misc] @@ -26,7 +26,7 @@ except ImportError: # WebRTC transport (browsers + native clients) — requires aiortc>=1.9 try: - from .webrtc_server import WebRTCTransport, WebRTCPeerSession + from .webrtc_server import WebRTCPeerSession, WebRTCTransport WEBRTC_AVAILABLE = True except ImportError: WebRTCTransport = None # type: ignore[assignment,misc] diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_client.py b/packages/meshbay-node/src/meshbay_node/transport/quic_client.py index af87b70..273f225 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/quic_client.py +++ b/packages/meshbay-node/src/meshbay_node/transport/quic_client.py @@ -13,18 +13,14 @@ import base64 import logging import os import struct -from pathlib import Path -import jwt import msgpack -from aioquic.asyncio import connect, QuicConnectionProtocol +from aioquic.asyncio import QuicConnectionProtocol, connect from aioquic.quic.configuration import QuicConfiguration from aioquic.quic.events import QuicEvent, StreamDataReceived from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - from meshbay_common import MNP_VERSION from meshbay_common.groupbox import PURPOSE_ACK, PURPOSE_INDEX, unseal -from meshbay_common.protocol import MNP, file_chunk_plaintext from meshbay_common.handshake import ( MNP_MIN_SUPPORTED, NONCE_LEN, @@ -36,6 +32,7 @@ from meshbay_common.handshake import ( quic_binding, verify_proof, ) +from meshbay_common.protocol import MNP, file_chunk_plaintext def _peer_cert_der(proto) -> bytes | None: diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py index 2a2b07a..036574b 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py @@ -24,17 +24,16 @@ import os import struct import uuid from pathlib import Path -from typing import Any, Callable +from typing import Any -import jwt import msgpack from aioquic.asyncio import QuicConnectionProtocol, serve from aioquic.quic.configuration import QuicConfiguration from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common import MNP_VERSION -from meshbay_node.roots import ROOT_NOT_SERVED, RootSet, entry_abs_path +from meshbay_common.crypto import pk_to_b64 +from meshbay_common.groupbox import PURPOSE_ACK, seal from meshbay_common.handshake import ( MNP_MIN_SUPPORTED, NONCE_LEN, @@ -48,10 +47,10 @@ from meshbay_common.handshake import ( quic_binding, verify_proof, ) -from meshbay_common.crypto import pk_to_b64 -from meshbay_common.groupbox import PURPOSE_ACK, seal from meshbay_common.protocol import MNP, file_chunk_wire + from meshbay_node.indexer import GroupIndex +from meshbay_node.roots import ROOT_NOT_SERVED, RootSet, entry_abs_path from meshbay_node.transport.wire import index_sync_message log = logging.getLogger(__name__) diff --git a/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py b/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py index 8d680ea..cfc93c7 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py +++ b/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py @@ -10,11 +10,10 @@ the QUIC handshake proof (11.5.6), since QUIC has no DTLS fingerprint to bind to Certificate is generated once and cached at ~/.config/meshbay/node_tls.crt/.key. """ -import logging -import os -from pathlib import Path import datetime import ipaddress +import logging +from pathlib import Path from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization @@ -47,8 +46,8 @@ def generate_self_signed_cert( .issuer_name(issuer) .public_key(rsa_key.public_key()) .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.datetime.now(datetime.timezone.utc)) - .not_valid_after(datetime.datetime.now(datetime.timezone.utc) + .not_valid_before(datetime.datetime.now(datetime.UTC)) + .not_valid_after(datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=3650)) .add_extension( x509.SubjectAlternativeName([ 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 8a5bbff..2bd1419 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -25,8 +25,6 @@ Signaling flow (handled externally by the hub): import asyncio import base64 import contextvars -import hashlib -import hmac import logging import os import re @@ -38,63 +36,53 @@ from pathlib import Path from typing import Any import blake3 -import jwt import msgpack -from aiortc import RTCPeerConnection, RTCSessionDescription, RTCDataChannel +from aiortc import RTCDataChannel, RTCPeerConnection, RTCSessionDescription from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, ) - from meshbay_common import MNP_VERSION -from meshbay_common.handshake import ( - MNP_MIN_SUPPORTED, - NONCE_LEN, - ROLE_CLIENT, - ROLE_NODE, - HandshakeError, - authorize_token, - check_version, - handshake_transcript, - make_proof, - verify_proof, - webrtc_binding, -) from meshbay_common.adminop import ( ADMIN_CHALLENGE_TTL, + OP_APP_DIRECTORIES, + OP_APPS_ENABLED, + OP_CHAT_DIRECTORY, + OP_CHAT_EPOCH, + OP_CHAT_LINK_PREVIEW, OP_DIR_DELETE, OP_FILE_DELETE, + OP_GEK_ROTATE, + OP_GROUP_ATTACH, + OP_GROUP_DETACH, OP_INVITE_CREATE, OP_MEMBER_REVOKE, - OP_GEK_ROTATE, OP_MEMBER_UNPIN, - OP_APPS_ENABLED, + OP_MUSICBRAINZ_ENABLED, + OP_ROOT_ADD, + OP_ROOT_EJECT, + OP_ROOT_PLUG, + OP_ROOT_REMOVE, + OP_ROOT_UPDATE, + OP_SEARCH_LISTED, OP_SET_SCAN_SETTINGS, - OP_TRANSFER_LIMITS, OP_TMDB_CONFIG, OP_TMDB_ENABLED, OP_TMDB_OVERRIDE, OP_TMDB_REMATCH, - OP_MUSICBRAINZ_ENABLED, - OP_APP_DIRECTORIES, - OP_CHAT_DIRECTORY, - OP_CHAT_EPOCH, - OP_CHAT_LINK_PREVIEW, - OP_SEARCH_LISTED, - OP_ROOT_ADD, - OP_ROOT_REMOVE, - OP_ROOT_UPDATE, - OP_ROOT_EJECT, - OP_ROOT_PLUG, - OP_GROUP_ATTACH, - OP_GROUP_DETACH, + OP_TRANSFER_LIMITS, admin_transcript, ) +from meshbay_common.chatbox import ( + NONCE_LEN as CHAT_NONCE_LEN, +) +from meshbay_common.chatbox import ( + SIG_LEN as CHAT_SIG_LEN, +) from meshbay_common.crypto import pk_to_b64, wrap_gek_aes from meshbay_common.device import ( DEVICE_TTL, device_add_transcript, - device_code_hash, device_hello_transcript, device_request_transcript, ) @@ -104,44 +92,62 @@ from meshbay_common.groupbox import ( PURPOSE_ROSTER, seal, ) +from meshbay_common.handshake import ( + MNP_MIN_SUPPORTED, + NONCE_LEN, + ROLE_CLIENT, + ROLE_NODE, + HandshakeError, + authorize_token, + check_version, + handshake_transcript, + make_proof, + verify_proof, + webrtc_binding, +) from meshbay_common.join import ( JOIN_TTL, ROLE_MEMBER, ROLE_OPERATOR, join_transcript, ) -from meshbay_common.chatbox import ( - NONCE_LEN as CHAT_NONCE_LEN, - SIG_LEN as CHAT_SIG_LEN, -) from meshbay_common.protocol import ( MNP, + UPLOAD_PROBE_INDEX, chunk_ciphertext, file_chunk_wire, - UPLOAD_PROBE_INDEX, file_upload_ack_wire, file_upload_payload, ) -from meshbay_node.chat import FORMAT_SEALED_V1, ReplayedMessage -from meshbay_node.transport.wire import index_sync_message -from meshbay_node.indexer import GroupIndex -from meshbay_node.indexer.indexer import DirectoryIndexer + from meshbay_node import hwaccel, linkpreview, ops, platform from meshbay_node import transfers as transfers_mod from meshbay_node import uploads as uploads_mod -from meshbay_node.transfers import TransferSlots +from meshbay_node.chat import FORMAT_SEALED_V1, ReplayedMessage +from meshbay_node.indexer import GroupIndex +from meshbay_node.indexer.indexer import DirectoryIndexer + # Re-imported under its original name: every call site and existing test in # this module still refers to it as `_probe_video`. The implementation lives # in media_probe.py so the indexer package (imported just above) can call it # too, for index-time enrichment, without a circular import. from meshbay_node.media_probe import ( BROWSER_INCOMPATIBLE_VIDEO_CODECS, +) +from meshbay_node.media_probe import ( probe_video as _probe_video, ) from meshbay_node.roots import ( - ROOT_NOT_SERVED, RootSet, entry_abs_path, off_disk, SAFE_UPLOAD_NAME, safe_subdir, + ROOT_NOT_SERVED, + SAFE_UPLOAD_NAME, + RootSet, _free_name, + entry_abs_path, + off_disk, + safe_subdir, ) +from meshbay_node.transfers import TransferSlots +from meshbay_node.transport.wire import index_sync_message log = logging.getLogger(__name__) @@ -6160,7 +6166,7 @@ class WebRTCPeerSession: # total budget is unchanged. await asyncio.wait_for(self._stream_credit_evt.wait(), timeout=STREAM_CREDIT_POLL) - except asyncio.TimeoutError: + except TimeoutError: silent = time.monotonic() - self._stream_heard_at if silent >= STREAM_CREDIT_TIMEOUT: log.info("Stream stalled: nothing from peer=%s for %.0fs", @@ -6208,7 +6214,7 @@ class WebRTCPeerSession: await asyncio.wait_for(asyncio.shield(prev), timeout=15) log.info("stream: previous stream ended in %.1fs", time.monotonic() - t0) - except asyncio.TimeoutError: + except TimeoutError: log.warning("stream: previous stream STILL RUNNING after 15s") except Exception: pass # it failed on its own; the slot is free either way @@ -6851,7 +6857,7 @@ async def _transcode_audio_to_aac(file_path: Path) -> bytes: try: _, stderr = await asyncio.wait_for( proc.communicate(), timeout=AUDIO_TRANSCODE_TIMEOUT_SECS) - except asyncio.TimeoutError: + except TimeoutError: proc.kill() await proc.wait() raise RuntimeError(f"ffmpeg timed out after {AUDIO_TRANSCODE_TIMEOUT_SECS}s") @@ -6912,7 +6918,7 @@ async def _seek_lands_at(file_path: Path, t: float, map_args: list[str]) -> floa ) stdout, _ = await asyncio.wait_for( probe.communicate(), timeout=SEEK_PROBE_TIMEOUT_SECS) - except (asyncio.TimeoutError, OSError) as e: + except (TimeoutError, OSError) as e: log.warning("stream: seek probe failed at %.1fs: %r", t, e) return None finally: @@ -6967,7 +6973,7 @@ async def _extract_subtitle_to_webvtt(file_path: Path, ordinal: int, try: _, stderr = await asyncio.wait_for( proc.communicate(), timeout=timeout) - except asyncio.TimeoutError: + except TimeoutError: proc.kill() await proc.wait() raise RuntimeError(f"ffmpeg timed out after {timeout:.0f}s") @@ -7152,7 +7158,7 @@ class WebRTCTransport: Returns (answer_sdp, ice_candidates) to relay back via hub signaling. ICE candidates are embedded in the SDP (aiortc gathers before returning). """ - from aiortc import RTCIceServer, RTCConfiguration + from aiortc import RTCConfiguration, RTCIceServer # aiortc keeps only the first STUN entry it sees here; the actual # multi-server fan-out is done by transport/stun_multi, which patches diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py index 77491a0..888487c 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/app.py +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -16,8 +16,8 @@ import logging from fastapi import FastAPI, HTTPException, Query from fastapi.responses import JSONResponse - from meshbay_common.background import spawn + from meshbay_node import __version__, ops from meshbay_node.indexer.indexer import DirectoryIndexer diff --git a/packages/meshbay-node/src/meshbay_node/uploads.py b/packages/meshbay-node/src/meshbay_node/uploads.py index f8ae7f9..dd31e1e 100644 --- a/packages/meshbay-node/src/meshbay_node/uploads.py +++ b/packages/meshbay-node/src/meshbay_node/uploads.py @@ -25,9 +25,9 @@ build first. from __future__ import annotations import time +from collections.abc import Iterable from dataclasses import dataclass, field from pathlib import Path -from typing import Iterable # What an unfinished upload is called on disk while it is being written. The # node has always used this; it is named here because the reaper below has to diff --git a/packages/meshbay-node/tests/test_admin_ops_mnp.py b/packages/meshbay-node/tests/test_admin_ops_mnp.py index 92e50b5..7fd1c2b 100644 --- a/packages/meshbay-node/tests/test_admin_ops_mnp.py +++ b/packages/meshbay-node/tests/test_admin_ops_mnp.py @@ -15,13 +15,10 @@ thing that finishes a revocation: the ex-member still holds the current key. """ import base64 -import time from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from conftest import one_root from meshbay_common.adminop import ( OP_GEK_ROTATE, OP_MEMBER_UNPIN, @@ -34,6 +31,8 @@ from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import open_roster from meshbay_node.transport.webrtc_server import WebRTCPeerSession +from conftest import one_root + GROUP = "g" * 32 diff --git a/packages/meshbay-node/tests/test_app_directories.py b/packages/meshbay-node/tests/test_app_directories.py index 726f5bd..955a769 100644 --- a/packages/meshbay-node/tests/test_app_directories.py +++ b/packages/meshbay-node/tests/test_app_directories.py @@ -26,7 +26,6 @@ from types import SimpleNamespace import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.adminop import OP_APP_DIRECTORIES from meshbay_node import ops from meshbay_node.indexer.group_index import GroupIndex diff --git a/packages/meshbay-node/tests/test_app_directories_signed.py b/packages/meshbay-node/tests/test_app_directories_signed.py index eda09d8..af362a1 100644 --- a/packages/meshbay-node/tests/test_app_directories_signed.py +++ b/packages/meshbay-node/tests/test_app_directories_signed.py @@ -24,7 +24,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.adminop import OP_APP_DIRECTORIES, admin_transcript from meshbay_common.crypto import pk_to_b64 from meshbay_common.join import ROLE_OPERATOR diff --git a/packages/meshbay-node/tests/test_apps_enabled_policy.py b/packages/meshbay-node/tests/test_apps_enabled_policy.py index 40c7cc8..1fa3d5c 100644 --- a/packages/meshbay-node/tests/test_apps_enabled_policy.py +++ b/packages/meshbay-node/tests/test_apps_enabled_policy.py @@ -13,12 +13,11 @@ way an unsigned upload is refused, so the check has to happen up front. from pathlib import Path import pytest - +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.adminop import OP_APPS_ENABLED from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import Roster from meshbay_node.transport.webrtc_server import WebRTCPeerSession -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from conftest import one_root diff --git a/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py b/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py index 251617c..b8c4dd6 100644 --- a/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py +++ b/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py @@ -17,10 +17,9 @@ import os import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_node import ops -from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig +from meshbay_node.config import Config, GroupConfig, HubConfig, KeystoreConfig, NodeConfig from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer from meshbay_node.indexer.enrich_audio import AudioEnricher diff --git a/packages/meshbay-node/tests/test_chat_is_bounded.py b/packages/meshbay-node/tests/test_chat_is_bounded.py index 4e767bc..5dc1128 100644 --- a/packages/meshbay-node/tests/test_chat_is_bounded.py +++ b/packages/meshbay-node/tests/test_chat_is_bounded.py @@ -33,7 +33,6 @@ import os import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.chatbox import NONCE_LEN, SIG_LEN from meshbay_common.crypto import pk_to_b64 from meshbay_node.chat import FORMAT_SEALED_V1, ChatStore diff --git a/packages/meshbay-node/tests/test_chat_multidevice.py b/packages/meshbay-node/tests/test_chat_multidevice.py index bb61285..058544a 100644 --- a/packages/meshbay-node/tests/test_chat_multidevice.py +++ b/packages/meshbay-node/tests/test_chat_multidevice.py @@ -16,10 +16,9 @@ account where it should be keyed by connection" mistake as `pin_identity`'s old INSERT OR REPLACE. """ -from pathlib import Path - import base64 import hashlib +from pathlib import Path from aiortc import RTCPeerConnection from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey diff --git a/packages/meshbay-node/tests/test_chat_pagination.py b/packages/meshbay-node/tests/test_chat_pagination.py index caf1f73..f3949a9 100644 --- a/packages/meshbay-node/tests/test_chat_pagination.py +++ b/packages/meshbay-node/tests/test_chat_pagination.py @@ -13,12 +13,10 @@ The cursor is the row id rather than the timestamp. `timestamp` is a float from timestamp cursor would then skip a message or return it twice. """ -import asyncio from pathlib import Path import pytest import pytest_asyncio - from meshbay_node.chat.store import ChatStore diff --git a/packages/meshbay-node/tests/test_chat_store.py b/packages/meshbay-node/tests/test_chat_store.py index d74310d..2533921 100644 --- a/packages/meshbay-node/tests/test_chat_store.py +++ b/packages/meshbay-node/tests/test_chat_store.py @@ -2,10 +2,9 @@ Tests for the SQLite-backed chat message store. """ + import pytest import pytest_asyncio -from pathlib import Path - from meshbay_node.chat.store import ChatStore diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py index ad60b91..cf2fd6e 100644 --- a/packages/meshbay-node/tests/test_cli_dispatch.py +++ b/packages/meshbay-node/tests/test_cli_dispatch.py @@ -14,10 +14,8 @@ branch nobody ever ran. """ import sys -from pathlib import Path import pytest - from meshbay_node import daemon as daemon_mod # Each verb, with the arguments that reach its branch. `--yes` where the command @@ -155,7 +153,6 @@ def test_the_verb_list_here_matches_the_parser(): A verb added to the parser and not to this file would go untested, which is exactly how `reload` shipped broken. """ - import argparse import inspect source = inspect.getsource(daemon_mod.main) diff --git a/packages/meshbay-node/tests/test_daemon.py b/packages/meshbay-node/tests/test_daemon.py index 8bd169d..8c4da2d 100644 --- a/packages/meshbay-node/tests/test_daemon.py +++ b/packages/meshbay-node/tests/test_daemon.py @@ -9,20 +9,21 @@ Hub interaction is mocked. import asyncio import base64 import os +from unittest.mock import AsyncMock, MagicMock, patch import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey -from unittest.mock import AsyncMock, MagicMock, patch - from meshbay_common.crypto import generate_gek from meshbay_common.groupbox import PURPOSE_INDEX, unseal -from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig -from conftest import one_root +from meshbay_node.config import Config, GroupConfig, HubConfig, KeystoreConfig, NodeConfig from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer +from conftest import one_root + + def _mock_keystore_keys(sk_ed): """Create a mock keystore with real Ed25519 + X25519 key material.""" sk_x = X25519PrivateKey.generate() @@ -125,7 +126,7 @@ async def test_daemon_creates_chat_store(tmp_path, node_config, gek, hub_pk_pem) patch("signal.SIGTERM", 15): try: await asyncio.wait_for(daemon.run(), timeout=5) - except (asyncio.TimeoutError, Exception): + except (TimeoutError, Exception): pass task = asyncio.create_task(run_daemon()) @@ -204,7 +205,7 @@ async def test_daemon_no_groups_stays_up(tmp_path, hub_pk_pem): patch("signal.SIGTERM", 15): try: await asyncio.wait_for(daemon.run(), timeout=5) - except (asyncio.TimeoutError, Exception): + except (TimeoutError, Exception): pass task = asyncio.create_task(run_daemon()) diff --git a/packages/meshbay-node/tests/test_device_linking.py b/packages/meshbay-node/tests/test_device_linking.py index 6d50580..53387ca 100644 --- a/packages/meshbay-node/tests/test_device_linking.py +++ b/packages/meshbay-node/tests/test_device_linking.py @@ -25,8 +25,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from conftest import one_root from meshbay_common.crypto import pk_to_b64 from meshbay_common.device import ( device_add_transcript, @@ -39,6 +37,8 @@ from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import generate_code, normalize_code, open_roster from meshbay_node.transport.webrtc_server import WebRTCPeerSession +from conftest import one_root + GROUP = "g" * 32 NONCE = b"\x11" * 32 diff --git a/packages/meshbay-node/tests/test_enrich.py b/packages/meshbay-node/tests/test_enrich.py index e059bbe..ca03392 100644 --- a/packages/meshbay-node/tests/test_enrich.py +++ b/packages/meshbay-node/tests/test_enrich.py @@ -3,15 +3,15 @@ import asyncio import shutil import subprocess -from pathlib import Path - import sys +from pathlib import Path import pytest - from meshbay_common.protocol import IndexEntry from meshbay_node.indexer.enrich import ( - Enricher, _season_and_show_from_ancestors, _synthetic_episode_number, + Enricher, + _season_and_show_from_ancestors, + _synthetic_episode_number, _title_from_siblings, ) from meshbay_node.media_cache import MediaCache diff --git a/packages/meshbay-node/tests/test_enrich_photo.py b/packages/meshbay-node/tests/test_enrich_photo.py index 81367eb..fb0530c 100644 --- a/packages/meshbay-node/tests/test_enrich_photo.py +++ b/packages/meshbay-node/tests/test_enrich_photo.py @@ -6,11 +6,10 @@ from pathlib import Path import piexif import pytest -from PIL import Image - from meshbay_common.protocol import IndexEntry from meshbay_node.indexer.enrich_photo import PhotoEnricher from meshbay_node.media_cache import MediaCache +from PIL import Image @pytest.fixture diff --git a/packages/meshbay-node/tests/test_group_roster.py b/packages/meshbay-node/tests/test_group_roster.py index a41cce2..74908e7 100644 --- a/packages/meshbay-node/tests/test_group_roster.py +++ b/packages/meshbay-node/tests/test_group_roster.py @@ -23,8 +23,6 @@ import time import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from conftest import one_root from meshbay_common.crypto import generate_gek, pk_to_b64 from meshbay_common.device import device_add_transcript from meshbay_common.groupbox import PURPOSE_ROSTER, unseal @@ -34,6 +32,8 @@ from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import open_roster from meshbay_node.transport.webrtc_server import WebRTCPeerSession +from conftest import one_root + GROUP = "g" * 32 NONCE = b"\x11" * 32 diff --git a/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py b/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py index fc6af70..fb9eade 100644 --- a/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py +++ b/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py @@ -18,17 +18,15 @@ import asyncio import base64 import os from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch +import meshbay_node.indexer.indexer as indexer_mod import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey -from unittest.mock import AsyncMock, MagicMock, patch - from meshbay_node import ops -from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig from meshbay_node.daemon import NodeDaemon -import meshbay_node.indexer.indexer as indexer_mod def _free_port() -> int: @@ -149,7 +147,7 @@ async def test_hot_loaded_group_finishes_scanning_without_anyone_awaiting_the_re with patch("signal.SIGINT", 2), patch("signal.SIGTERM", 15): try: await asyncio.wait_for(daemon.run(), timeout=15) - except (asyncio.TimeoutError, Exception): + except (TimeoutError, Exception): pass run_task = asyncio.create_task(run_daemon()) @@ -270,7 +268,7 @@ async def test_group_scoped_ops_404_until_listed_then_succeed(tmp_path): with patch("signal.SIGINT", 2), patch("signal.SIGTERM", 15): try: await asyncio.wait_for(daemon.run(), timeout=15) - except (asyncio.TimeoutError, Exception): + except (TimeoutError, Exception): pass run_task = asyncio.create_task(run_daemon()) diff --git a/packages/meshbay-node/tests/test_hub_client.py b/packages/meshbay-node/tests/test_hub_client.py index 2975ee8..e733610 100644 --- a/packages/meshbay-node/tests/test_hub_client.py +++ b/packages/meshbay-node/tests/test_hub_client.py @@ -2,24 +2,17 @@ Tests for meshbay_node.hub_client — uses httpx.MockTransport to avoid network. """ -import json -import os import time -import pytest -from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch import httpx import jwt +import pytest +from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey -from cryptography.hazmat.primitives import serialization - - -from meshbay_node.hub_client import HubClient, HubConfig, HubSession +from meshbay_node.hub_client import HubClient, HubConfig from meshbay_node.keystore import NodeKeys - # ── Test fixtures ────────────────────────────────────────────────────────────── @pytest.fixture diff --git a/packages/meshbay-node/tests/test_hub_ws_group_claim.py b/packages/meshbay-node/tests/test_hub_ws_group_claim.py index 49ac776..98b944b 100644 --- a/packages/meshbay-node/tests/test_hub_ws_group_claim.py +++ b/packages/meshbay-node/tests/test_hub_ws_group_claim.py @@ -15,7 +15,6 @@ import asyncio import json import pytest - from meshbay_node.hub_client import HubClient, HubConfig, HubSession diff --git a/packages/meshbay-node/tests/test_ice_filter.py b/packages/meshbay-node/tests/test_ice_filter.py index a995c2b..0fec2d2 100644 --- a/packages/meshbay-node/tests/test_ice_filter.py +++ b/packages/meshbay-node/tests/test_ice_filter.py @@ -3,8 +3,8 @@ transport/ice_filter — the interface filter must name the same adapter on every platform, and must never leave the gather with no address at all. """ -import ifaddr import aioice.ice +import ifaddr import pytest from meshbay_node.transport import ice_filter diff --git a/packages/meshbay-node/tests/test_index_cache.py b/packages/meshbay-node/tests/test_index_cache.py index 036b4d7..8f0ff25 100644 --- a/packages/meshbay-node/tests/test_index_cache.py +++ b/packages/meshbay-node/tests/test_index_cache.py @@ -1,7 +1,6 @@ """Tests for the (path, size, mtime) -> hash cache (indexer/cache.py).""" import pytest - from meshbay_node.indexer.cache import IndexCache diff --git a/packages/meshbay-node/tests/test_index_delta_carries_roots.py b/packages/meshbay-node/tests/test_index_delta_carries_roots.py index 227f2d0..c939b4a 100644 --- a/packages/meshbay-node/tests/test_index_delta_carries_roots.py +++ b/packages/meshbay-node/tests/test_index_delta_carries_roots.py @@ -19,14 +19,12 @@ Additive on the wire (MNP 1.1): a 1.0 client sees a field it does not read. from pathlib import Path from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_common.groupbox import PURPOSE_INDEX, unseal from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import RootSet from meshbay_node.transport.wire import index_delta_message, index_sync_message - GROUP = "g" * 32 diff --git a/packages/meshbay-node/tests/test_index_no_cleartext.py b/packages/meshbay-node/tests/test_index_no_cleartext.py index e5e4555..30d1226 100644 --- a/packages/meshbay-node/tests/test_index_no_cleartext.py +++ b/packages/meshbay-node/tests/test_index_no_cleartext.py @@ -14,7 +14,6 @@ framing or in a field name. import msgpack import pytest -from conftest import one_root from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek from meshbay_common.groupbox import PURPOSE_ACK, PURPOSE_INDEX, seal, unseal @@ -22,6 +21,8 @@ from meshbay_common.protocol import MNP from meshbay_node.indexer import DirectoryIndexer, GroupIndex from meshbay_node.transport.wire import index_delta_message, index_sync_message +from conftest import one_root + # A filename and a folder name that appear nowhere else in the tree. SECRET_FILE = "quixotry-ledger-2019.pdf" SECRET_DIR = "zarfwidget-archive" diff --git a/packages/meshbay-node/tests/test_index_progress.py b/packages/meshbay-node/tests/test_index_progress.py index df51e85..ecdc223 100644 --- a/packages/meshbay-node/tests/test_index_progress.py +++ b/packages/meshbay-node/tests/test_index_progress.py @@ -6,16 +6,15 @@ itself (see test_daemon.py for that) and never anything sent to the hub. """ import asyncio +from unittest.mock import MagicMock import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from unittest.mock import MagicMock from fastapi.testclient import TestClient - -from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig +from meshbay_node.config import Config, HubConfig, KeystoreConfig, NodeConfig from meshbay_node.daemon import NodeDaemon -from meshbay_node.indexer.indexer import IndexProgress from meshbay_node.indexer.group_index import GroupIndex +from meshbay_node.indexer.indexer import IndexProgress from meshbay_node.transport.webrtc_server import WebRTCPeerSession from meshbay_node.ui.app import create_ui_app diff --git a/packages/meshbay-node/tests/test_indexer.py b/packages/meshbay-node/tests/test_indexer.py index 6aee1b5..48bf399 100644 --- a/packages/meshbay-node/tests/test_indexer.py +++ b/packages/meshbay-node/tests/test_indexer.py @@ -4,16 +4,15 @@ import asyncio import os import threading import time -import pytest from pathlib import Path -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey +import meshbay_node.indexer.indexer as indexer_mod +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek from meshbay_node.indexer import DirectoryIndexer, GroupIndex, IndexCache -import meshbay_node.indexer.indexer as indexer_mod + from conftest import one_root -from meshbay_node.keystore import NodeKeys @pytest.fixture @@ -741,7 +740,7 @@ def test_small_file_gets_hash_version_1(tmp_path): def test_large_file_gets_hash_version_2(tmp_path): - from meshbay_node.indexer.indexer import _scan_file, _PARTIAL_THRESHOLD + from meshbay_node.indexer.indexer import _PARTIAL_THRESHOLD, _scan_file d = tmp_path / "root" d.mkdir() f = d / "big.mkv" @@ -755,7 +754,7 @@ def test_large_file_gets_hash_version_2(tmp_path): def test_file_at_threshold_gets_hash_version_1(tmp_path): - from meshbay_node.indexer.indexer import _scan_file, _PARTIAL_THRESHOLD + from meshbay_node.indexer.indexer import _PARTIAL_THRESHOLD, _scan_file d = tmp_path / "root" d.mkdir() f = d / "exact.mkv" @@ -770,7 +769,7 @@ def test_partial_hash_differs_from_full_hash(tmp_path): """For a file above the threshold, the partial hash must differ from what a full-file blake3 would produce (they read different bytes).""" import blake3 as b3 - from meshbay_node.indexer.indexer import _scan_file, _PARTIAL_THRESHOLD + from meshbay_node.indexer.indexer import _PARTIAL_THRESHOLD, _scan_file d = tmp_path / "root" d.mkdir() f = d / "big.mkv" @@ -785,7 +784,7 @@ def test_partial_hash_differs_from_full_hash(tmp_path): def test_partial_hash_is_deterministic(tmp_path): - from meshbay_node.indexer.indexer import _scan_file, _PARTIAL_THRESHOLD + from meshbay_node.indexer.indexer import _PARTIAL_THRESHOLD, _scan_file d = tmp_path / "root" d.mkdir() f = d / "big.mkv" diff --git a/packages/meshbay-node/tests/test_keystore.py b/packages/meshbay-node/tests/test_keystore.py index 7dfefce..b9dd3a4 100644 --- a/packages/meshbay-node/tests/test_keystore.py +++ b/packages/meshbay-node/tests/test_keystore.py @@ -3,15 +3,13 @@ import sys import pytest -from pathlib import Path +from meshbay_common.crypto import generate_gek from meshbay_node.keystore import ( - NodeKeys, create_keystore, load_keystore, - save_keystore, load_or_create_keystore, + save_keystore, ) -from meshbay_common.crypto import generate_gek def test_create_and_load(tmp_path): diff --git a/packages/meshbay-node/tests/test_leaseless_reads.py b/packages/meshbay-node/tests/test_leaseless_reads.py index 6bd7f1f..2c4c6cd 100644 --- a/packages/meshbay-node/tests/test_leaseless_reads.py +++ b/packages/meshbay-node/tests/test_leaseless_reads.py @@ -23,9 +23,10 @@ import re from pathlib import Path import pytest - from meshbay_node.transfers import ( - LEASELESS_IDLE_SECS, MAX_LEASELESS_IN_FLIGHT, LeaselessReads, + LEASELESS_IDLE_SECS, + MAX_LEASELESS_IN_FLIGHT, + LeaselessReads, ) SPA = (Path(__file__).resolve().parents[2] / "meshbay-hub" / "src" diff --git a/packages/meshbay-node/tests/test_media_cache.py b/packages/meshbay-node/tests/test_media_cache.py index dc78d8d..ec4e56e 100644 --- a/packages/meshbay-node/tests/test_media_cache.py +++ b/packages/meshbay-node/tests/test_media_cache.py @@ -3,8 +3,7 @@ import time import pytest - -from meshbay_node.media_cache import MediaCache, TMDB_META_TTL_SECS, MUSICBRAINZ_META_TTL_SECS +from meshbay_node.media_cache import MUSICBRAINZ_META_TTL_SECS, TMDB_META_TTL_SECS, MediaCache @pytest.fixture diff --git a/packages/meshbay-node/tests/test_media_cache_eviction.py b/packages/meshbay-node/tests/test_media_cache_eviction.py index 587063a..f0292b5 100644 --- a/packages/meshbay-node/tests/test_media_cache_eviction.py +++ b/packages/meshbay-node/tests/test_media_cache_eviction.py @@ -18,7 +18,6 @@ about `create_all()`. Every existing node has a `thumbs` table without it. import sqlite3 import pytest - from meshbay_node.media_cache import MediaCache diff --git a/packages/meshbay-node/tests/test_multi_group.py b/packages/meshbay-node/tests/test_multi_group.py index 3dc4778..e095426 100644 --- a/packages/meshbay-node/tests/test_multi_group.py +++ b/packages/meshbay-node/tests/test_multi_group.py @@ -7,19 +7,18 @@ Verifies that: - A user in both groups can access both """ -import os import time + import jwt import pytest -from pathlib import Path -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization - +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek, pk_to_b64 from meshbay_node.indexer import DirectoryIndexer -from conftest import one_root -from meshbay_node.transport.quic_server import QuicChunkServer from meshbay_node.transport.quic_client import QuicChunkClient +from meshbay_node.transport.quic_server import QuicChunkServer + +from conftest import one_root @pytest.fixture diff --git a/packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py b/packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py index 76b687e..00c1b69 100644 --- a/packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py +++ b/packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py @@ -13,12 +13,11 @@ The contact string stays node-wide — see test_musicbrainz_config_policy.py. from pathlib import Path import pytest - +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.adminop import OP_MUSICBRAINZ_ENABLED from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import Roster from meshbay_node.transport.webrtc_server import WebRTCPeerSession -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from conftest import one_root diff --git a/packages/meshbay-node/tests/test_node_status.py b/packages/meshbay-node/tests/test_node_status.py index 897be33..7a27623 100644 --- a/packages/meshbay-node/tests/test_node_status.py +++ b/packages/meshbay-node/tests/test_node_status.py @@ -12,22 +12,22 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from conftest import one_root from meshbay_common.adminop import ( - OP_ROOT_ADD, OP_ROOT_REMOVE, OP_GROUP_ATTACH, OP_MEMBER_UNPIN, + OP_MEMBER_UNPIN, admin_transcript, ) -from meshbay_node.transport.quic_server import Denylist from meshbay_common.crypto import pk_to_b64 from meshbay_common.join import ROLE_OPERATOR from meshbay_common.protocol import MNP from meshbay_node import ops from meshbay_node.indexer.group_index import GroupIndex -from meshbay_node.roster import open_roster from meshbay_node.roots import RootSet +from meshbay_node.roster import open_roster +from meshbay_node.transport.quic_server import Denylist from meshbay_node.transport.webrtc_server import WebRTCPeerSession +from conftest import one_root + GROUP = "g" * 32 @@ -260,7 +260,7 @@ async def test_add_root_creates_directory_and_returns_info(tmp_path): shared.mkdir() new_dir = tmp_path / "new_root" - from meshbay_node.config import NodeConfig, GroupConfig, RootSpec + from meshbay_node.config import GroupConfig, NodeConfig, RootSpec cfg = GroupConfig(id=GROUP, name="test", roots=[ RootSpec(path=str(shared), name="shared", kind="generic", writable=True), ]) @@ -300,7 +300,7 @@ async def test_remove_root_requires_at_least_one_remaining(tmp_path): shared = tmp_path / "shared" shared.mkdir() - from meshbay_node.config import GroupConfig, RootSpec, NodeConfig + from meshbay_node.config import GroupConfig, NodeConfig, RootSpec cfg = GroupConfig(id=GROUP, name="test", roots=[ RootSpec(path=str(shared), name="shared", kind="generic", writable=True), ]) @@ -333,7 +333,7 @@ async def test_removing_a_writable_root_is_allowed(tmp_path): d1.mkdir() d2.mkdir() - from meshbay_node.config import GroupConfig, RootSpec, NodeConfig + from meshbay_node.config import GroupConfig, NodeConfig, RootSpec cfg = GroupConfig(id=GROUP, name="test", roots=[ RootSpec(path=str(d1), name="incoming", kind="generic", writable=True), RootSpec(path=str(d2), name="shared", kind="generic", writable=False), @@ -369,7 +369,7 @@ async def test_update_root_rewrites_the_flags_in_node_toml(tmp_path): d1 = tmp_path / "media" d1.mkdir() - from meshbay_node.config import GroupConfig, RootSpec, NodeConfig + from meshbay_node.config import GroupConfig, NodeConfig, RootSpec cfg = GroupConfig(id=GROUP, name="test", roots=[ RootSpec(path=str(d1), name="media", kind="generic", writable=False), ]) @@ -420,7 +420,7 @@ async def test_update_root_replaces_a_legacy_upload_line(tmp_path): d1 = tmp_path / "media" d1.mkdir() - from meshbay_node.config import GroupConfig, RootSpec, NodeConfig + from meshbay_node.config import GroupConfig, NodeConfig, RootSpec cfg = GroupConfig(id=GROUP, name="test", roots=[ RootSpec(path=str(d1), name="media", kind="generic", writable=True), ]) @@ -452,7 +452,7 @@ async def test_remove_root_succeeds_with_two_roots(tmp_path): d1.mkdir() d2.mkdir() - from meshbay_node.config import GroupConfig, RootSpec, NodeConfig + from meshbay_node.config import GroupConfig, NodeConfig, RootSpec cfg = GroupConfig(id=GROUP, name="test", roots=[ RootSpec(path=str(d1), name="dir1", kind="generic", writable=True), RootSpec(path=str(d2), name="dir2", kind="generic", writable=False), diff --git a/packages/meshbay-node/tests/test_partial_uploads.py b/packages/meshbay-node/tests/test_partial_uploads.py index 0270a69..80ab454 100644 --- a/packages/meshbay-node/tests/test_partial_uploads.py +++ b/packages/meshbay-node/tests/test_partial_uploads.py @@ -23,21 +23,23 @@ from pathlib import Path from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek +from meshbay_common.protocol import ( + UPLOAD_PROBE_INDEX, + file_upload_ack_payload, +) from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import Root, RootSet from meshbay_node.transport.webrtc_server import WebRTCPeerSession - -from meshbay_common.protocol import ( - UPLOAD_PROBE_INDEX, file_upload_ack_payload, -) - -from conftest import one_root, sealed_upload - from meshbay_node.uploads import ( - ORPHAN_AFTER_SECS, PART_SUFFIX, PartialUploads, find_parts, orphaned_parts, + ORPHAN_AFTER_SECS, + PART_SUFFIX, + PartialUploads, + find_parts, + orphaned_parts, ) +from conftest import one_root, sealed_upload # ── the state ─────────────────────────────────────────────────────────────── @@ -466,7 +468,7 @@ async def test_an_upload_chunk_says_its_slot_is_in_use(tmp_path): The download twin of this was fixed a day earlier; the same omission was still here, invisible until uploads took a real lease. """ - from meshbay_node.transfers import TransferSlots, UPLOAD + from meshbay_node.transfers import UPLOAD, TransferSlots ctx = _group_ctx(tmp_path) peer = _peer(ctx) diff --git a/packages/meshbay-node/tests/test_peer_session_limits.py b/packages/meshbay-node/tests/test_peer_session_limits.py index 64961ad..d5c909b 100644 --- a/packages/meshbay-node/tests/test_peer_session_limits.py +++ b/packages/meshbay-node/tests/test_peer_session_limits.py @@ -15,10 +15,12 @@ import asyncio from unittest.mock import MagicMock import pytest - from meshbay_node.transport import webrtc_server as ws_mod from meshbay_node.transport.webrtc_server import ( - MAX_PEER_SESSIONS, UNAUTHENTICATED_SESSION_TIMEOUT, WebRTCTransport) + MAX_PEER_SESSIONS, + UNAUTHENTICATED_SESSION_TIMEOUT, + WebRTCTransport, +) def _transport() -> WebRTCTransport: diff --git a/packages/meshbay-node/tests/test_platform.py b/packages/meshbay-node/tests/test_platform.py index 3fb27f3..5c80c5b 100644 --- a/packages/meshbay-node/tests/test_platform.py +++ b/packages/meshbay-node/tests/test_platform.py @@ -5,8 +5,8 @@ here by monkeypatching that (and `os.environ`) rather than only on the OS the suite happens to run on. """ -import os import asyncio +import os import sys from pathlib import Path from unittest.mock import Mock diff --git a/packages/meshbay-node/tests/test_poster_cache.py b/packages/meshbay-node/tests/test_poster_cache.py index bbd824d..908fb69 100644 --- a/packages/meshbay-node/tests/test_poster_cache.py +++ b/packages/meshbay-node/tests/test_poster_cache.py @@ -15,7 +15,6 @@ already uses to resolve a thumbnail by id. """ import pytest - from meshbay_node.media_cache import MediaCache from meshbay_node.transport.webrtc_server import WebRTCPeerSession diff --git a/packages/meshbay-node/tests/test_quic_transport.py b/packages/meshbay-node/tests/test_quic_transport.py index fb28295..8eb0622 100644 --- a/packages/meshbay-node/tests/test_quic_transport.py +++ b/packages/meshbay-node/tests/test_quic_transport.py @@ -6,17 +6,17 @@ Same structure as test_transport.py but uses QUIC instead of TCP+TLS. import asyncio import os import time + import jwt import pytest -from pathlib import Path -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization - +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek, pk_to_b64 from meshbay_node.indexer import DirectoryIndexer -from conftest import one_root -from meshbay_node.transport.quic_server import QuicChunkServer, Denylist from meshbay_node.transport.quic_client import QuicChunkClient +from meshbay_node.transport.quic_server import Denylist, QuicChunkServer + +from conftest import one_root @pytest.fixture diff --git a/packages/meshbay-node/tests/test_rename_reenrichment.py b/packages/meshbay-node/tests/test_rename_reenrichment.py index f6761d9..7ddca6f 100644 --- a/packages/meshbay-node/tests/test_rename_reenrichment.py +++ b/packages/meshbay-node/tests/test_rename_reenrichment.py @@ -17,9 +17,8 @@ import asyncio import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek -from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig +from meshbay_node.config import Config, GroupConfig, HubConfig, KeystoreConfig, NodeConfig from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer from meshbay_node.media_cache import MediaCache diff --git a/packages/meshbay-node/tests/test_replug_restores_enrichment.py b/packages/meshbay-node/tests/test_replug_restores_enrichment.py index 04a06ae..a2d2db4 100644 --- a/packages/meshbay-node/tests/test_replug_restores_enrichment.py +++ b/packages/meshbay-node/tests/test_replug_restores_enrichment.py @@ -46,10 +46,8 @@ import os import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek -from meshbay_node.config import (Config, GroupConfig, HubConfig, KeystoreConfig, - NodeConfig) +from meshbay_node.config import Config, GroupConfig, HubConfig, KeystoreConfig, NodeConfig from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer from meshbay_node.indexer.enrich_audio import AudioEnricher diff --git a/packages/meshbay-node/tests/test_root_availability.py b/packages/meshbay-node/tests/test_root_availability.py index 2d848c7..82bf6bd 100644 --- a/packages/meshbay-node/tests/test_root_availability.py +++ b/packages/meshbay-node/tests/test_root_availability.py @@ -13,13 +13,11 @@ an indexer that treats a vanished root as a set of deletions, which is what the straightforward implementation does. """ -import asyncio import os from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_node.indexer.indexer import DirectoryIndexer from meshbay_node.roots import RootSet diff --git a/packages/meshbay-node/tests/test_root_eject.py b/packages/meshbay-node/tests/test_root_eject.py index f57fb92..d73e71c 100644 --- a/packages/meshbay-node/tests/test_root_eject.py +++ b/packages/meshbay-node/tests/test_root_eject.py @@ -21,7 +21,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_node.indexer.indexer import DirectoryIndexer from meshbay_node.roots import RootSet from meshbay_node.roster import Roster diff --git a/packages/meshbay-node/tests/test_root_ops_reach_the_live_set.py b/packages/meshbay-node/tests/test_root_ops_reach_the_live_set.py index 976af82..6a52c06 100644 --- a/packages/meshbay-node/tests/test_root_ops_reach_the_live_set.py +++ b/packages/meshbay-node/tests/test_root_ops_reach_the_live_set.py @@ -28,7 +28,6 @@ from types import SimpleNamespace import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_node import ops from meshbay_node.config import GroupConfig, NodeConfig, RootSpec from meshbay_node.indexer.group_index import GroupIndex diff --git a/packages/meshbay-node/tests/test_root_paths_are_operator_only.py b/packages/meshbay-node/tests/test_root_paths_are_operator_only.py index 080d4be..0e89afa 100644 --- a/packages/meshbay-node/tests/test_root_paths_are_operator_only.py +++ b/packages/meshbay-node/tests/test_root_paths_are_operator_only.py @@ -21,7 +21,6 @@ import inspect import re from pathlib import Path - from meshbay_node import daemon as daemon_mod from meshbay_node import ops from meshbay_node.roots import RootSet diff --git a/packages/meshbay-node/tests/test_root_writable_policy.py b/packages/meshbay-node/tests/test_root_writable_policy.py index 3c8a837..0cd9af8 100644 --- a/packages/meshbay-node/tests/test_root_writable_policy.py +++ b/packages/meshbay-node/tests/test_root_writable_policy.py @@ -26,15 +26,15 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from conftest import sealed_upload -from meshbay_common.adminop import OP_ROOT_UPDATE, OP_ROOT_EJECT, OP_ROOT_PLUG +from meshbay_common.adminop import OP_ROOT_EJECT, OP_ROOT_PLUG, OP_ROOT_UPDATE from meshbay_common.crypto import generate_gek from meshbay_common.protocol import MNP from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import RootSet from meshbay_node.transport.webrtc_server import WebRTCPeerSession +from conftest import sealed_upload + pytestmark = pytest.mark.asyncio diff --git a/packages/meshbay-node/tests/test_roots.py b/packages/meshbay-node/tests/test_roots.py index 9233180..004f004 100644 --- a/packages/meshbay-node/tests/test_roots.py +++ b/packages/meshbay-node/tests/test_roots.py @@ -7,15 +7,17 @@ file went to the wrong disk" or "the same film is listed twice and deleting one copy breaks the other". """ -from pathlib import Path import pytest - +from meshbay_common.protocol import IndexEntry from meshbay_node.roots import ( - Root, RootError, RootSet, entry_abs_path, - SAFE_UPLOAD_NAME, safe_subdir, _free_name, + SAFE_UPLOAD_NAME, + RootError, + RootSet, + _free_name, + entry_abs_path, + safe_subdir, ) -from meshbay_common.protocol import IndexEntry def _spec(path, **kw): diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py index 2e8c183..67c3b08 100644 --- a/packages/meshbay-node/tests/test_roster_pairing.py +++ b/packages/meshbay-node/tests/test_roster_pairing.py @@ -18,14 +18,13 @@ import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import generate_gek, pk_to_b64, unwrap_gek_aes from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR, join_transcript from meshbay_node.indexer.group_index import GroupIndex -from conftest import one_root from meshbay_node.roster import Roster, hash_code, normalize_code from meshbay_node.transport.webrtc_server import WebRTCPeerSession +from conftest import one_root # ── Fixtures ────────────────────────────────────────────────────────────────── @@ -694,7 +693,6 @@ async def test_unpinned_operator_loses_authority_immediately(tmp_path, roster): def _ui_client(tmp_path, roster, **extra): from fastapi.testclient import TestClient - from meshbay_node.config import Config from meshbay_node.ui.app import create_ui_app diff --git a/packages/meshbay-node/tests/test_scan_settings_policy.py b/packages/meshbay-node/tests/test_scan_settings_policy.py index 94f4421..926c94f 100644 --- a/packages/meshbay-node/tests/test_scan_settings_policy.py +++ b/packages/meshbay-node/tests/test_scan_settings_policy.py @@ -13,7 +13,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.adminop import OP_SET_SCAN_SETTINGS from meshbay_common.crypto import generate_gek from meshbay_node import ops diff --git a/packages/meshbay-node/tests/test_season_and_search_requests.py b/packages/meshbay-node/tests/test_season_and_search_requests.py index 4141d13..7e81e63 100644 --- a/packages/meshbay-node/tests/test_season_and_search_requests.py +++ b/packages/meshbay-node/tests/test_season_and_search_requests.py @@ -10,7 +10,6 @@ test_tmdb_override_policy.py. """ import pytest - from meshbay_common.protocol import MNP from meshbay_node.media_cache import MediaCache from meshbay_node.transport.webrtc_server import WebRTCPeerSession diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py index d6ecb71..bfe25b9 100644 --- a/packages/meshbay-node/tests/test_security_regressions.py +++ b/packages/meshbay-node/tests/test_security_regressions.py @@ -9,20 +9,18 @@ only ever exercised happy paths, never an authorization boundary. If one of these starts failing, a fix has been reverted. Do not "fix" the test. """ -import base64 import struct from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek -from meshbay_common.protocol import IndexEntry from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import RootSet -from conftest import one_root, opened_ack, sealed_upload from meshbay_node.transport.webrtc_server import WebRTCPeerSession +from conftest import one_root, opened_ack, sealed_upload + def _safe_name_re(): """ @@ -64,7 +62,7 @@ def test_daemon_exposes_no_plaintext_listener(): C1: the daemon must not bind anything that serves content without a handshake. NodeConfig no longer carries an HTTP port at all. """ - from meshbay_node.config import NodeConfig, GroupConfig + from meshbay_node.config import GroupConfig, NodeConfig assert "http_port" not in NodeConfig.__dataclass_fields__ assert "http_port" not in GroupConfig.__dataclass_fields__ @@ -568,6 +566,7 @@ def test_admin_signature_does_not_transfer_between_operations(tmp_path): def test_admin_challenge_expires(tmp_path): """H5: a stale challenge must not be usable.""" import time as _time + from meshbay_common.adminop import ADMIN_CHALLENGE_TTL, OP_FILE_DELETE session = _session(tmp_path, "operator") @@ -633,6 +632,7 @@ def test_keystore_records_argon2_params_for_migration(tmp_path): envelope records the parameters it was written with. """ import json + from meshbay_node.keystore import create_keystore, load_keystore path = tmp_path / "keystore.enc" @@ -648,11 +648,17 @@ def test_legacy_keystore_still_opens(tmp_path): """M2: a keystore written under the 64 MB profile must still unlock.""" import base64 as _b64 import json + import msgpack from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from meshbay_common.crypto import ( - LEGACY_ARGON2_ITERATIONS, LEGACY_ARGON2_LANES, LEGACY_ARGON2_MEMORY_COST, - derive_keystore_key, encrypt_keystore, pk_to_b64, sk_to_b64, + LEGACY_ARGON2_ITERATIONS, + LEGACY_ARGON2_LANES, + LEGACY_ARGON2_MEMORY_COST, + derive_keystore_key, + encrypt_keystore, + pk_to_b64, + sk_to_b64, ) from meshbay_node.keystore import load_keystore @@ -717,7 +723,9 @@ def test_pre_handshake_message_budget_is_small(): unauthenticated peer could announce a huge frame and dribble bytes into it. """ from meshbay_node.transport.webrtc_server import ( - MAX_MSG, PRE_HANDSHAKE_MAX_MSG, _DataChannelBuffer, + MAX_MSG, + PRE_HANDSHAKE_MAX_MSG, + _DataChannelBuffer, ) assert PRE_HANDSHAKE_MAX_MSG <= 1024 * 1024 assert PRE_HANDSHAKE_MAX_MSG < MAX_MSG @@ -918,8 +926,8 @@ async def test_an_identified_device_that_is_not_an_operator_is_refused(tmp_path) and `operator_pks()` is rebuilt from the roster on every call so a revoked one stops working at once. """ - from meshbay_node.roster import Roster from meshbay_common.crypto import pk_to_b64 + from meshbay_node.roster import Roster roster = Roster(db_path=tmp_path / "roster.db") await roster.open() @@ -940,9 +948,9 @@ async def test_an_identified_device_that_is_not_an_operator_is_refused(tmp_path) async def test_a_paired_operator_device_is_what_opens_it(tmp_path): """The positive case, so the test above is about authority and not about everything being refused.""" - from meshbay_node.roster import Roster - from meshbay_common.join import ROLE_OPERATOR from meshbay_common.crypto import pk_to_b64 + from meshbay_common.join import ROLE_OPERATOR + from meshbay_node.roster import Roster roster = Roster(db_path=tmp_path / "roster.db") await roster.open() diff --git a/packages/meshbay-node/tests/test_startup_scan_enrichment.py b/packages/meshbay-node/tests/test_startup_scan_enrichment.py index f43c6e9..f3ad2ca 100644 --- a/packages/meshbay-node/tests/test_startup_scan_enrichment.py +++ b/packages/meshbay-node/tests/test_startup_scan_enrichment.py @@ -21,12 +21,9 @@ is exercised the same way a real operator's group would be. import asyncio import pytest -from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from unittest.mock import MagicMock - from meshbay_common.crypto import generate_gek -from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig +from meshbay_node.config import Config, GroupConfig, HubConfig, KeystoreConfig, NodeConfig from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer from meshbay_node.indexer.enrich import Enricher diff --git a/packages/meshbay-node/tests/test_stream_audio_track_selection.py b/packages/meshbay-node/tests/test_stream_audio_track_selection.py index 53781f9..b9e2105 100644 --- a/packages/meshbay-node/tests/test_stream_audio_track_selection.py +++ b/packages/meshbay-node/tests/test_stream_audio_track_selection.py @@ -23,7 +23,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node.indexer.group_index import GroupIndex diff --git a/packages/meshbay-node/tests/test_stream_audio_transcode.py b/packages/meshbay-node/tests/test_stream_audio_transcode.py index 92518ba..6c9bde7 100644 --- a/packages/meshbay-node/tests/test_stream_audio_transcode.py +++ b/packages/meshbay-node/tests/test_stream_audio_transcode.py @@ -22,7 +22,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node.indexer.group_index import GroupIndex @@ -44,7 +43,7 @@ def _make_clip(path: Path, *, acodec: str, channels: int = 2) -> None: subprocess.run( ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "testsrc=size=320x240:rate=25:duration=1", - "-f", "lavfi", "-i", f"sine=frequency=440:duration=1:sample_rate=48000", + "-f", "lavfi", "-i", "sine=frequency=440:duration=1:sample_rate=48000", "-ac", str(channels), "-c:v", "libx264", "-preset", "ultrafast", "-c:a", acodec, str(path)], diff --git a/packages/meshbay-node/tests/test_stream_capacity.py b/packages/meshbay-node/tests/test_stream_capacity.py index a35ece8..7ce6bf6 100644 --- a/packages/meshbay-node/tests/test_stream_capacity.py +++ b/packages/meshbay-node/tests/test_stream_capacity.py @@ -17,9 +17,10 @@ find. import asyncio import pytest - from meshbay_node.transport.webrtc_server import ( - MAX_CONCURRENT_TRANSCODES, WebRTCPeerSession, WebRTCTransport, + MAX_CONCURRENT_TRANSCODES, + WebRTCPeerSession, + WebRTCTransport, ) @@ -35,11 +36,11 @@ def transport(tmp_path): """A real WebRTCTransport. Its keys and index are genuine but incidental — nothing below the capacity code reads them.""" from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - - from conftest import one_root from meshbay_common.crypto import generate_gek from meshbay_node.indexer.group_index import GroupIndex + from conftest import one_root + sk_node = Ed25519PrivateKey.generate() gek = generate_gek() shared = tmp_path / "shared" diff --git a/packages/meshbay-node/tests/test_stream_capacity_config.py b/packages/meshbay-node/tests/test_stream_capacity_config.py index 442d3ef..c788ba6 100644 --- a/packages/meshbay-node/tests/test_stream_capacity_config.py +++ b/packages/meshbay-node/tests/test_stream_capacity_config.py @@ -23,7 +23,6 @@ import textwrap from pathlib import Path import pytest - from meshbay_node.config import load_config from meshbay_node.roots import RootSet from meshbay_node.transport.webrtc_server import ( diff --git a/packages/meshbay-node/tests/test_stream_seek_audio_alignment.py b/packages/meshbay-node/tests/test_stream_seek_audio_alignment.py index c1d0c66..5d78c00 100644 --- a/packages/meshbay-node/tests/test_stream_seek_audio_alignment.py +++ b/packages/meshbay-node/tests/test_stream_seek_audio_alignment.py @@ -29,7 +29,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node.indexer.group_index import GroupIndex diff --git a/packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py b/packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py index 9aea2b4..c575572 100644 --- a/packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py +++ b/packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py @@ -35,7 +35,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node.indexer.group_index import GroupIndex diff --git a/packages/meshbay-node/tests/test_stream_subtitle_tracks.py b/packages/meshbay-node/tests/test_stream_subtitle_tracks.py index d3bcc8b..7a27a37 100644 --- a/packages/meshbay-node/tests/test_stream_subtitle_tracks.py +++ b/packages/meshbay-node/tests/test_stream_subtitle_tracks.py @@ -33,7 +33,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node.indexer.group_index import GroupIndex diff --git a/packages/meshbay-node/tests/test_stream_video_transcode.py b/packages/meshbay-node/tests/test_stream_video_transcode.py index 316fdce..b60d546 100644 --- a/packages/meshbay-node/tests/test_stream_video_transcode.py +++ b/packages/meshbay-node/tests/test_stream_video_transcode.py @@ -32,7 +32,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node import hwaccel diff --git a/packages/meshbay-node/tests/test_title_parse.py b/packages/meshbay-node/tests/test_title_parse.py index 2c4573a..95c798b 100644 --- a/packages/meshbay-node/tests/test_title_parse.py +++ b/packages/meshbay-node/tests/test_title_parse.py @@ -17,7 +17,6 @@ from meshbay_node.indexer.title_parse import ( year_in, ) - # ── §3.3 row: plain, well-formed movie filename ────────────────────────────── def test_plain_movie_filename_parses_confidently(): diff --git a/packages/meshbay-node/tests/test_tmdb.py b/packages/meshbay-node/tests/test_tmdb.py index 299ff2a..981357a 100644 --- a/packages/meshbay-node/tests/test_tmdb.py +++ b/packages/meshbay-node/tests/test_tmdb.py @@ -2,7 +2,6 @@ import httpx import pytest - from meshbay_node.tmdb import TmdbClient diff --git a/packages/meshbay-node/tests/test_tmdb_config_policy.py b/packages/meshbay-node/tests/test_tmdb_config_policy.py index cef4fda..6a51eb0 100644 --- a/packages/meshbay-node/tests/test_tmdb_config_policy.py +++ b/packages/meshbay-node/tests/test_tmdb_config_policy.py @@ -21,12 +21,11 @@ wire (see _issue_admin_challenge's docstring). from pathlib import Path import pytest - +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.adminop import OP_TMDB_CONFIG from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import Roster from meshbay_node.transport.webrtc_server import WebRTCPeerSession -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from conftest import one_root diff --git a/packages/meshbay-node/tests/test_tmdb_enabled_policy.py b/packages/meshbay-node/tests/test_tmdb_enabled_policy.py index 0a748cd..10d1e2b 100644 --- a/packages/meshbay-node/tests/test_tmdb_enabled_policy.py +++ b/packages/meshbay-node/tests/test_tmdb_enabled_policy.py @@ -14,12 +14,11 @@ test_tmdb_config_policy.py for those. from pathlib import Path import pytest - +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.adminop import OP_TMDB_ENABLED from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import Roster from meshbay_node.transport.webrtc_server import WebRTCPeerSession -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from conftest import one_root diff --git a/packages/meshbay-node/tests/test_tmdb_language_fallback.py b/packages/meshbay-node/tests/test_tmdb_language_fallback.py index 52fe4c4..62171c3 100644 --- a/packages/meshbay-node/tests/test_tmdb_language_fallback.py +++ b/packages/meshbay-node/tests/test_tmdb_language_fallback.py @@ -9,7 +9,6 @@ field, or silently showing a blank overview/poster. """ import pytest - from meshbay_node.transport.webrtc_server import WebRTCPeerSession pytestmark = pytest.mark.asyncio diff --git a/packages/meshbay-node/tests/test_tmdb_override_policy.py b/packages/meshbay-node/tests/test_tmdb_override_policy.py index d434e1a..aed3ea2 100644 --- a/packages/meshbay-node/tests/test_tmdb_override_policy.py +++ b/packages/meshbay-node/tests/test_tmdb_override_policy.py @@ -14,9 +14,8 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.adminop import OP_TMDB_OVERRIDE -from meshbay_common.protocol import IndexEntry, MNP +from meshbay_common.protocol import MNP, IndexEntry from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.media_cache import MediaCache from meshbay_node.transport.webrtc_server import WebRTCPeerSession diff --git a/packages/meshbay-node/tests/test_tmdb_rematch_policy.py b/packages/meshbay-node/tests/test_tmdb_rematch_policy.py index 198d75b..4d43552 100644 --- a/packages/meshbay-node/tests/test_tmdb_rematch_policy.py +++ b/packages/meshbay-node/tests/test_tmdb_rematch_policy.py @@ -9,7 +9,6 @@ asking for a fresh resolution. import hashlib import pytest -from conftest import one_root from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.adminop import OP_TMDB_REMATCH from meshbay_common.protocol import MNP, IndexEntry @@ -17,6 +16,8 @@ from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.media_cache import MediaCache from meshbay_node.transport.webrtc_server import WebRTCPeerSession +from conftest import one_root + pytestmark = pytest.mark.asyncio diff --git a/packages/meshbay-node/tests/test_tmdb_show_director.py b/packages/meshbay-node/tests/test_tmdb_show_director.py index cb31c2c..fd324b1 100644 --- a/packages/meshbay-node/tests/test_tmdb_show_director.py +++ b/packages/meshbay-node/tests/test_tmdb_show_director.py @@ -12,7 +12,6 @@ the show details. Several creators is ordinary, so they join into one line. """ import pytest - from meshbay_node.transport.webrtc_server import WebRTCPeerSession pytestmark = pytest.mark.asyncio diff --git a/packages/meshbay-node/tests/test_transfer_settings.py b/packages/meshbay-node/tests/test_transfer_settings.py index bcfa6a8..a2c7fd9 100644 --- a/packages/meshbay-node/tests/test_transfer_settings.py +++ b/packages/meshbay-node/tests/test_transfer_settings.py @@ -19,10 +19,12 @@ shape of the bug this whole branch started from (`webrtc._stream_sem`). """ import pytest - from meshbay_node.roster import Roster from meshbay_node.transfers import ( - DEFAULT_MAX_PER_MEMBER, DOWNLOAD, UPLOAD, TransferSlots, + DEFAULT_MAX_PER_MEMBER, + DOWNLOAD, + UPLOAD, + TransferSlots, ) diff --git a/packages/meshbay-node/tests/test_transfer_slots.py b/packages/meshbay-node/tests/test_transfer_slots.py index 7056e93..08e7e70 100644 --- a/packages/meshbay-node/tests/test_transfer_slots.py +++ b/packages/meshbay-node/tests/test_transfer_slots.py @@ -21,12 +21,18 @@ by reasoning about it. import random import pytest - from meshbay_node.transfers import ( - DOWNLOAD, GRANT_DEADLINE_SECS, IDLE_TIMEOUT_SECS, KINDS, - MAX_MISSED_GRANTS, MAX_QUEUED_PER_MEMBER, REASON_ABANDONED, REASON_IDLE, - REASON_NOT_TAKEN_UP, TransferSlots, + DOWNLOAD, + GRANT_DEADLINE_SECS, + IDLE_TIMEOUT_SECS, + KINDS, + MAX_MISSED_GRANTS, + MAX_QUEUED_PER_MEMBER, + REASON_ABANDONED, + REASON_IDLE, + REASON_NOT_TAKEN_UP, UPLOAD, + TransferSlots, ) diff --git a/packages/meshbay-node/tests/test_transport_wire_parity.py b/packages/meshbay-node/tests/test_transport_wire_parity.py index bc6b134..6e74b28 100644 --- a/packages/meshbay-node/tests/test_transport_wire_parity.py +++ b/packages/meshbay-node/tests/test_transport_wire_parity.py @@ -16,7 +16,6 @@ again. import inspect import pytest -from conftest import one_root from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek from meshbay_common.groupbox import PURPOSE_INDEX, unseal @@ -25,6 +24,8 @@ from meshbay_node.indexer import DirectoryIndexer from meshbay_node.transport import quic_server, webrtc_server from meshbay_node.transport.wire import index_sync_message +from conftest import one_root + @pytest.fixture def gek(): diff --git a/packages/meshbay-node/tests/test_upload_size_cap.py b/packages/meshbay-node/tests/test_upload_size_cap.py index dca6e15..cc04eba 100644 --- a/packages/meshbay-node/tests/test_upload_size_cap.py +++ b/packages/meshbay-node/tests/test_upload_size_cap.py @@ -17,7 +17,6 @@ import textwrap from pathlib import Path import pytest - from meshbay_node.config import load_config from meshbay_node.roots import RootSet from meshbay_node.transport.webrtc_server import ( diff --git a/packages/meshbay-node/tests/test_video_root_gates_enrichment.py b/packages/meshbay-node/tests/test_video_root_gates_enrichment.py index d06b3f4..c39876a 100644 --- a/packages/meshbay-node/tests/test_video_root_gates_enrichment.py +++ b/packages/meshbay-node/tests/test_video_root_gates_enrichment.py @@ -17,10 +17,9 @@ import asyncio import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import generate_gek from meshbay_node import ops -from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig +from meshbay_node.config import Config, GroupConfig, HubConfig, KeystoreConfig, NodeConfig from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer from meshbay_node.indexer.enrich import Enricher diff --git a/packages/meshbay-node/tests/test_webrtc_transport.py b/packages/meshbay-node/tests/test_webrtc_transport.py index 0839d42..4aba99f 100644 --- a/packages/meshbay-node/tests/test_webrtc_transport.py +++ b/packages/meshbay-node/tests/test_webrtc_transport.py @@ -9,52 +9,59 @@ Uses local loopback (no STUN/ICE needed for localhost). import asyncio import base64 -from contextlib import asynccontextmanager import hashlib import hmac import os import struct import time +from contextlib import asynccontextmanager import jwt import msgpack import pytest +from aiortc import RTCPeerConnection, RTCSessionDescription from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import ( - Ed25519PrivateKey, Ed25519PublicKey, + Ed25519PrivateKey, + Ed25519PublicKey, ) -from aiortc import RTCPeerConnection, RTCSessionDescription - from meshbay_common import MNP_VERSION from meshbay_common.crypto import ( generate_gek, pk_to_b64, - wrap_gek, - wrap_gek_aes, unwrap_gek, unwrap_gek_aes, + wrap_gek, + wrap_gek_aes, ) from meshbay_common.groupbox import PURPOSE_ACK, PURPOSE_INDEX, unseal -from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_common.protocol import MNP +from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes + TEST_GROUP = "g" -from meshbay_common.handshake import ( - NONCE_LEN, ROLE_CLIENT, ROLE_NODE, handshake_transcript, - make_proof, verify_proof, webrtc_binding, -) from meshbay_common.adminop import ( OP_FILE_DELETE, OP_INVITE_CREATE, admin_transcript, ) +from meshbay_common.handshake import ( + NONCE_LEN, + ROLE_CLIENT, + ROLE_NODE, + handshake_transcript, + make_proof, + verify_proof, + webrtc_binding, +) from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR, join_transcript from meshbay_node.bundle_store import BundleStore -from conftest import one_root -from meshbay_node.roster import Roster from meshbay_node.indexer import DirectoryIndexer +from meshbay_node.roster import Roster from meshbay_node.transport.webrtc_server import WebRTCTransport +from conftest import one_root + @pytest.fixture def sk_node(): diff --git a/packages/meshbay-node/tests/test_windows_root_shapes.py b/packages/meshbay-node/tests/test_windows_root_shapes.py index 5c5da25..56279b1 100644 --- a/packages/meshbay-node/tests/test_windows_root_shapes.py +++ b/packages/meshbay-node/tests/test_windows_root_shapes.py @@ -27,7 +27,6 @@ import tomllib from pathlib import Path, PureWindowsPath import pytest - from meshbay_node.roots import RootError, RootSet, derive_name BS = chr(92) diff --git a/packages/meshbay-node/tests/test_wizard_apps_endpoint.py b/packages/meshbay-node/tests/test_wizard_apps_endpoint.py index 10ab489..77fa9a6 100644 --- a/packages/meshbay-node/tests/test_wizard_apps_endpoint.py +++ b/packages/meshbay-node/tests/test_wizard_apps_endpoint.py @@ -13,12 +13,12 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from fastapi.testclient import TestClient - -from conftest import one_root from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import Roster from meshbay_node.ui.app import create_ui_app +from conftest import one_root + pytestmark = pytest.mark.asyncio diff --git a/packages/meshbay-node/tests/transfer_probe.py b/packages/meshbay-node/tests/transfer_probe.py index 38e6cb6..7aa4902 100755 --- a/packages/meshbay-node/tests/transfer_probe.py +++ b/packages/meshbay-node/tests/transfer_probe.py @@ -69,9 +69,8 @@ if not (_QE / "e2e.py").exists(): f"run this from a machine that has one.") sys.path.insert(0, str(_QE)) -import httpx # noqa: E402 - -from e2e import Client, env # noqa: E402 +import httpx # noqa: E402 +from e2e import Client, env # noqa: E402 def _line(ok: bool, text: str) -> None: @@ -272,7 +271,6 @@ async def operator_checks(client, ack, group, node_id) -> int: and the operator's view because it reported the module defaults. """ import asyncio as _a - import json as _json import re as _re failures = 0 @@ -322,7 +320,7 @@ async def operator_checks(client, ack, group, node_id) -> int: try: started = await _a.wait_for( _wait_for_grant(client, tr_waiting), timeout=20) - except _a.TimeoutError: + except TimeoutError: started = False _line(started, "raising the cap started the waiting transfer, with no " "restart and no reconnection") @@ -371,7 +369,7 @@ async def operator_checks(client, ack, group, node_id) -> int: _cli("transfers", "per-member", "4", "4", "--group", group["id"]) try: moved = await _a.wait_for(_wait_for_grant(client, tr_c), timeout=20) - except _a.TimeoutError: + except TimeoutError: moved = False _line(moved, "raising the per-member cap started what was waiting on it") failures += not moved @@ -526,7 +524,7 @@ async def probe(args) -> int: try: seen.append(await alice.recv_type("transfer_state", timeout=15)) - except asyncio.TimeoutError: + except TimeoutError: break promoted = [m for m in seen if m.get("state") == "granted"] ok = bool(promoted) -- 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-node') 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 From 95dd3dc13aecec85c2fd72410cd4d1f4ed582dfa Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 19 Sep 2026 17:26:07 +0200 Subject: refactor(node): delete the revocation subscriber H4 replaced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RevocationSubscriber` kept its revocations in two in-memory sets. That is the defect **H4** names: revocations that live only in memory mean a node restart silently un-revokes everyone, and group revocations were dropped entirely. The fix was the persisted `Denylist`, fed from `hub_client.maintain_ws` through `daemon.py`'s `on_revocation`, which verifies the token against the hub key and calls `deny_user` / `deny_group`. So this is not merely unused code. It is the version the register records as wrong, left beside the one that replaced it — and its docstring still opens with "Usage in daemon:" and three lines of instructions for wiring it up, which is the worst shape dead code takes: it reads as the way to do the thing. **L7** is about exactly this, and the index cache removed two days ago was the same shape. Certainty before deleting, since that was the condition: no import of it anywhere, static or dynamic; no entry point; nothing in packaging, the systemd units, the man page, QE or the documentation; and no test covers it. Every module in the three packages still imports, ruff passes, and the suite is 2893 — unchanged. Co-Authored-By: Claude Opus 5 --- .../meshbay-node/src/meshbay_node/revocation.py | 149 --------------------- 1 file changed, 149 deletions(-) delete mode 100644 packages/meshbay-node/src/meshbay_node/revocation.py (limited to 'packages/meshbay-node') diff --git a/packages/meshbay-node/src/meshbay_node/revocation.py b/packages/meshbay-node/src/meshbay_node/revocation.py deleted file mode 100644 index dede5d0..0000000 --- a/packages/meshbay-node/src/meshbay_node/revocation.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -MeshBay Node — revocation subscriber. - -Maintains a persistent WebSocket connection to the hub. -When a signed revocation token arrives, verifies it (Ed25519) -and adds the revoked target to the local blocklist. - -Usage in daemon: - subscriber = RevocationSubscriber(hub_url, access_token, hub_pk_pem) - await subscriber.start() # connects in background - # check membership: - if subscriber.is_revoked("user", user_id): - refuse connection - await subscriber.stop() -""" - -import asyncio -import json -import logging -from typing import Literal - -import jwt - -log = logging.getLogger(__name__) - -RevocationTarget = Literal["user", "group"] - - -class RevocationSubscriber: - """ - Background task that keeps a WebSocket connection to the hub - and maintains a local revocation set. - """ - - def __init__( - self, - hub_url: str, - access_token: str, - hub_pk_pem: bytes, - reconnect_delay: float = 5.0, - ): - self._hub_url = hub_url.rstrip("/") - self._access_token = access_token - self._hub_pk_pem = hub_pk_pem - self._reconnect_delay = reconnect_delay - self._revoked_users: set[str] = set() - self._revoked_groups: set[str] = set() - self._task: asyncio.Task | None = None - self._running = False - - def is_revoked(self, target: RevocationTarget, target_id: str) -> bool: - if target == "user": - return target_id in self._revoked_users - return target_id in self._revoked_groups - - def add_revocation(self, target: RevocationTarget, target_id: str) -> None: - if target == "user": - self._revoked_users.add(target_id) - log.warning("User revoked locally: %s", target_id[:8]) - else: - self._revoked_groups.add(target_id) - log.warning("Group revoked locally: %s", target_id[:8]) - - def verify_and_apply(self, token: str) -> bool: - """Verify a revocation token and apply it. Returns True if valid.""" - try: - from meshbay_common.handshake import JWT_LEEWAY_SECONDS - payload = jwt.decode(token, self._hub_pk_pem, algorithms=["EdDSA"], - leeway=JWT_LEEWAY_SECONDS, - options={"verify_exp": False}) - if payload.get("type") != "revocation": - return False - target = payload["target"] - target_id = payload["target_id"] - self.add_revocation(target, target_id) - return True - except Exception as e: - log.error("Invalid revocation token: %s", e) - return False - - async def start(self) -> None: - self._running = True - self._task = asyncio.create_task(self._run_loop()) - log.info("RevocationSubscriber started") - - async def stop(self) -> None: - self._running = False - if self._task: - self._task.cancel() - try: - await self._task - except asyncio.CancelledError: - pass - log.info("RevocationSubscriber stopped") - - async def _run_loop(self) -> None: - while self._running: - try: - await self._connect_and_listen() - except asyncio.CancelledError: - raise - except Exception as e: - log.warning("WS disconnected (%s), reconnecting in %ss", e, self._reconnect_delay) - await asyncio.sleep(self._reconnect_delay) - - async def _connect_and_listen(self) -> None: - ws_url = self._hub_url.replace("http://", "ws://").replace("https://", "wss://") - ws_url += "/v1/nodes/ws" - - # 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 - await ws.send(json.dumps({ - "type": "auth", - "token": self._access_token, - })) - auth_resp = json.loads(await ws.recv()) - if auth_resp.get("type") != "auth_ok": - raise ConnectionError(f"WS auth failed: {auth_resp}") - log.info("WS connected to hub — node_id=%s", auth_resp.get("node_id", "?")[:8]) - - # Listen for revocations + send keepalive pings - ping_interval = 30.0 - last_ping = asyncio.get_event_loop().time() - - while self._running: - now = asyncio.get_event_loop().time() - if now - last_ping > ping_interval: - await ws.send(json.dumps({"type": "ping"})) - last_ping = now - - try: - msg_raw = await asyncio.wait_for(ws.recv(), timeout=ping_interval + 5) - msg = json.loads(msg_raw) - - if msg.get("type") == "revocation": - token = msg.get("token", "") - ok = self.verify_and_apply(token) - log.info("Revocation received — valid=%s", ok) - elif msg.get("type") == "pong": - pass - else: - log.debug("WS message: %s", msg.get("type")) - - except TimeoutError: - continue -- cgit v1.2.3