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 --- .../src/meshbay_common/background.py | 3 +- .../meshbay-common/src/meshbay_common/crypto.py | 10 +- .../meshbay-common/src/meshbay_common/keyderive.py | 6 +- .../meshbay-common/src/meshbay_common/protocol.py | 4 +- .../meshbay-common/src/meshbay_common/webcrypto.py | 3 +- .../meshbay-common/tests/test_background_tasks.py | 1 - packages/meshbay-common/tests/test_handshake.py | 5 +- .../meshbay-common/tests/test_js_python_parity.py | 6 -- packages/meshbay-common/tests/test_keyderive.py | 4 +- packages/meshbay-common/tests/test_paths.py | 2 - packages/meshbay-common/tests/test_webcrypto.py | 16 ++- packages/meshbay-hub/src/meshbay_hub/api/admin.py | 5 +- packages/meshbay-hub/src/meshbay_hub/api/deps.py | 2 +- .../meshbay-hub/src/meshbay_hub/api/federation.py | 12 +-- packages/meshbay-hub/src/meshbay_hub/api/groups.py | 21 ++-- packages/meshbay-hub/src/meshbay_hub/api/hub.py | 2 +- .../meshbay-hub/src/meshbay_hub/api/moderation.py | 1 - packages/meshbay-hub/src/meshbay_hub/api/nodes.py | 10 +- .../src/meshbay_hub/api/notifications.py | 6 +- .../meshbay-hub/src/meshbay_hub/api/revocation.py | 20 ++-- .../meshbay-hub/src/meshbay_hub/api/signaling.py | 4 +- packages/meshbay-hub/src/meshbay_hub/api/users.py | 49 +++++---- packages/meshbay-hub/src/meshbay_hub/app.py | 42 ++++---- packages/meshbay-hub/src/meshbay_hub/auth.py | 4 +- packages/meshbay-hub/src/meshbay_hub/csam.py | 5 +- .../meshbay-hub/src/meshbay_hub/db/__init__.py | 4 +- .../src/meshbay_hub/db/migrations/env.py | 6 +- .../a7b8c9d0e1f2_add_group_last_activity.py | 11 +-- .../versions/a9b8c7d6e5f4_add_login_throttle.py | 8 +- .../versions/b1c2d3e4f5a6_add_hub_settings.py | 11 +-- .../c3d4e5f6a7b8_group_name_unique_per_owner.py | 11 +-- .../versions/d28b9caf9f07_initial_schema.py | 11 +-- .../d4e5f6a7b8c9_add_email_verification.py | 11 +-- .../versions/e5f6a7b8c9d0_add_mail_quota.py | 8 +- .../versions/f1a2b3c4d5e6_add_user_preferences.py | 11 +-- packages/meshbay-hub/src/meshbay_hub/db/models.py | 14 ++- .../meshbay-hub/src/meshbay_hub/login_throttle.py | 10 +- packages/meshbay-hub/src/meshbay_hub/mail.py | 24 ++--- .../meshbay-hub/src/meshbay_hub/tasks/cleanup.py | 10 +- packages/meshbay-hub/tests/conftest.py | 14 ++- .../meshbay-hub/tests/harness/chat_send_probe.py | 1 - .../meshbay-hub/tests/test_account_deletion.py | 3 +- packages/meshbay-hub/tests/test_account_pinning.py | 1 - packages/meshbay-hub/tests/test_admin_views.py | 6 +- .../meshbay-hub/tests/test_asset_versioning.py | 1 - .../tests/test_availability_between_members.py | 8 +- .../meshbay-hub/tests/test_captcha_host_check.py | 2 - packages/meshbay-hub/tests/test_device_auth.py | 2 - packages/meshbay-hub/tests/test_group_hosting.py | 21 ++-- .../tests/test_group_leave_and_quota.py | 7 +- .../meshbay-hub/tests/test_group_membership.py | 3 +- packages/meshbay-hub/tests/test_group_purge.py | 13 ++- .../meshbay-hub/tests/test_groups_self_service.py | 8 +- packages/meshbay-hub/tests/test_hub_api.py | 29 +++--- packages/meshbay-hub/tests/test_login_lockout.py | 9 +- .../meshbay-hub/tests/test_mail_is_not_a_relay.py | 13 +-- .../tests/test_migrations_reach_head.py | 3 +- packages/meshbay-hub/tests/test_node_auth.py | 2 +- packages/meshbay-hub/tests/test_node_ws_auth.py | 6 +- .../tests/test_notification_dismissal.py | 1 - packages/meshbay-hub/tests/test_notifications.py | 1 - .../tests/test_notifications_behaviour.py | 5 +- .../meshbay-hub/tests/test_packaging_hub_unit.py | 1 - packages/meshbay-hub/tests/test_password_change.py | 3 +- packages/meshbay-hub/tests/test_password_reset.py | 4 +- .../meshbay-hub/tests/test_public_groups_toggle.py | 4 +- packages/meshbay-hub/tests/test_revocation.py | 4 +- .../meshbay-hub/tests/test_session_lifetime.py | 13 ++- packages/meshbay-hub/tests/test_session_renewal.py | 2 +- .../meshbay-hub/tests/test_table_rows_measured.py | 3 +- 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 +-- poc/spike1_crypto.py | 15 +-- poc/spike2_hub.py | 11 ++- poc/spike3_node.py | 17 ++-- poc/spike4_nat.py | 26 +++-- poc/spike5_client.py | 16 ++- poc/spike5_node.py | 16 ++- poc/spike6_gek.py | 20 ++-- 173 files changed, 680 insertions(+), 685 deletions(-) diff --git a/packages/meshbay-common/src/meshbay_common/background.py b/packages/meshbay-common/src/meshbay_common/background.py index 1f25dfd..0afc3e8 100644 --- a/packages/meshbay-common/src/meshbay_common/background.py +++ b/packages/meshbay-common/src/meshbay_common/background.py @@ -30,7 +30,8 @@ from __future__ import annotations import asyncio import logging -from typing import Any, Coroutine +from collections.abc import Coroutine +from typing import Any log = logging.getLogger(__name__) diff --git a/packages/meshbay-common/src/meshbay_common/crypto.py b/packages/meshbay-common/src/meshbay_common/crypto.py index eadb42b..e537500 100644 --- a/packages/meshbay-common/src/meshbay_common/crypto.py +++ b/packages/meshbay-common/src/meshbay_common/crypto.py @@ -5,17 +5,17 @@ Validated in Spike 1 and Spike 6 of the POC. All operations use PyCA cryptography (OpenSSL-backed, hardware-accelerated). """ -import os import base64 +import os +import blake3 +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.kdf.argon2 import Argon2id -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.primitives import hashes, serialization -import blake3 +from cryptography.hazmat.primitives.kdf.hkdf import HKDF # ── Key serialisation helpers ───────────────────────────────────────────────── diff --git a/packages/meshbay-common/src/meshbay_common/keyderive.py b/packages/meshbay-common/src/meshbay_common/keyderive.py index 497f877..4b90af3 100644 --- a/packages/meshbay-common/src/meshbay_common/keyderive.py +++ b/packages/meshbay-common/src/meshbay_common/keyderive.py @@ -24,11 +24,11 @@ See keyderive.js for the browser-side implementation. """ import hashlib + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from cryptography.hazmat.primitives.kdf.argon2 import Argon2id - # Argon2id parameters — same as keystore (see crypto.py) _ITERATIONS = 3 _MEMORY_COST = 65536 # 64 MB — increase to 262144 for production @@ -85,9 +85,11 @@ def encrypt_keypair_bundle( Returns: AES-256-GCM ciphertext (nonce prepended). """ import os + + import msgpack from cryptography.hazmat.primitives.ciphers.aead import AESGCM + from meshbay_common.crypto import sk_to_raw - import msgpack # Derive an AES key from the password (different info string from key derivation) salt = hashlib.sha256(f"meshbay:bundle:v1:{username}".encode()).digest() diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py index c444711..d6e30e7 100644 --- a/packages/meshbay-common/src/meshbay_common/protocol.py +++ b/packages/meshbay-common/src/meshbay_common/protocol.py @@ -32,12 +32,11 @@ which local promise a reply belongs to. import os from dataclasses import dataclass, field -from typing import Any # The wire versions live in meshbay_common/__init__.py — one source, because a # second copy here said "0.1" while every message on the wire carried "0.2". # Nothing imported it, which is the only reason it was harmless. -from meshbay_common import MNP_VERSION, MHP_VERSION # noqa: F401 (re-export) +from meshbay_common import MHP_VERSION, MNP_VERSION # noqa: F401 (re-export) from meshbay_common.groupbox import PURPOSE_UPLOAD, seal, unseal from meshbay_common.webcrypto import ( chunk_key_aes, @@ -45,7 +44,6 @@ from meshbay_common.webcrypto import ( encrypt_chunk_aes, ) - # ── MNP message types ───────────────────────────────────────────────────────── class MNP: diff --git a/packages/meshbay-common/src/meshbay_common/webcrypto.py b/packages/meshbay-common/src/meshbay_common/webcrypto.py index 58958f8..3bd5ae6 100644 --- a/packages/meshbay-common/src/meshbay_common/webcrypto.py +++ b/packages/meshbay-common/src/meshbay_common/webcrypto.py @@ -26,9 +26,10 @@ This ensures AES and ChaCha20 keys are always distinct even from the same GEK. """ import os + +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives import hashes def chunk_key_aes(gek: bytes, file_hash: bytes, chunk_index: int) -> bytes: diff --git a/packages/meshbay-common/tests/test_background_tasks.py b/packages/meshbay-common/tests/test_background_tasks.py index 4f0ac59..d01235b 100644 --- a/packages/meshbay-common/tests/test_background_tasks.py +++ b/packages/meshbay-common/tests/test_background_tasks.py @@ -28,7 +28,6 @@ import logging from pathlib import Path import pytest - from meshbay_common import background REPO = Path(__file__).resolve().parents[3] diff --git a/packages/meshbay-common/tests/test_handshake.py b/packages/meshbay-common/tests/test_handshake.py index d4f6d2b..11313aa 100644 --- a/packages/meshbay-common/tests/test_handshake.py +++ b/packages/meshbay-common/tests/test_handshake.py @@ -12,7 +12,6 @@ import jwt import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.handshake import ( HANDSHAKE_PREFIX, NONCE_LEN, @@ -207,8 +206,8 @@ def test_membership_refusal_carries_a_code_a_client_can_act_on(): is exactly the kind of coupling that breaks when someone improves a message. """ import jwt as _jwt - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey sk = Ed25519PrivateKey.generate() pem_priv = sk.private_bytes( @@ -234,8 +233,8 @@ def test_a_group_this_node_does_not_host_is_refused_with_a_code(): dark on 2026-09-11 with its real host online — or had to match on wording. """ import jwt as _jwt - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey sk = Ed25519PrivateKey.generate() pem_priv = sk.private_bytes( diff --git a/packages/meshbay-common/tests/test_js_python_parity.py b/packages/meshbay-common/tests/test_js_python_parity.py index 9893a9f..3887414 100644 --- a/packages/meshbay-common/tests/test_js_python_parity.py +++ b/packages/meshbay-common/tests/test_js_python_parity.py @@ -21,7 +21,6 @@ import tempfile from pathlib import Path import pytest - from meshbay_common.adminop import admin_transcript from meshbay_common.handshake import handshake_transcript, webrtc_binding from meshbay_common.join import join_transcript @@ -312,7 +311,6 @@ def _groupbox_payload(idx: int) -> dict: @pytest.fixture(scope="module") def groupbox_js(tmp_path_factory): import msgpack - from meshbay_common.groupbox import seal d = tmp_path_factory.mktemp("groupbox-parity") @@ -486,7 +484,6 @@ def chatbox_js(tmp_path_factory): from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, ) - from meshbay_common.chatbox import seal d = tmp_path_factory.mktemp("chatbox-parity") @@ -537,7 +534,6 @@ def test_browser_opens_a_chat_message_python_sealed(idx, vector, chatbox_js): @pytest.mark.parametrize("idx,vector", list(enumerate(CHAT_VECTORS))) def test_python_opens_a_chat_message_the_browser_sealed(idx, vector, chatbox_js): import msgpack - from meshbay_common.chatbox import open_message js, vectors = chatbox_js @@ -580,7 +576,6 @@ def test_a_message_does_not_open_under_another_epoch(chatbox_js): would round-trip against itself and pass every other test here. """ import pytest as _pytest - from meshbay_common.chatbox import open_message _js, vectors = chatbox_js @@ -598,7 +593,6 @@ def test_a_message_does_not_open_under_another_devices_key(chatbox_js): any coordination — the property per-device ratchet chains were wanted for. """ import pytest as _pytest - from meshbay_common.chatbox import open_message _js, vectors = chatbox_js diff --git a/packages/meshbay-common/tests/test_keyderive.py b/packages/meshbay-common/tests/test_keyderive.py index 0aa6201..40b3c71 100644 --- a/packages/meshbay-common/tests/test_keyderive.py +++ b/packages/meshbay-common/tests/test_keyderive.py @@ -1,12 +1,12 @@ """Tests for password-based key derivation.""" import pytest +from meshbay_common.crypto import pk_to_b64 from meshbay_common.keyderive import ( + decrypt_keypair_bundle, derive_keys_from_password, encrypt_keypair_bundle, - decrypt_keypair_bundle, ) -from meshbay_common.crypto import pk_to_b64 def test_deterministic(): diff --git a/packages/meshbay-common/tests/test_paths.py b/packages/meshbay-common/tests/test_paths.py index b9052e3..223a2a6 100644 --- a/packages/meshbay-common/tests/test_paths.py +++ b/packages/meshbay-common/tests/test_paths.py @@ -7,7 +7,6 @@ one file, and whether a member can save what they downloaded. """ import pytest - from meshbay_common.paths import ( find_fold_collisions, fold, @@ -18,7 +17,6 @@ from meshbay_common.paths import ( sanitize_for_download, ) - # ── Same file or not ───────────────────────────────────────────────────────── def test_case_differences_fold_together(): diff --git a/packages/meshbay-common/tests/test_webcrypto.py b/packages/meshbay-common/tests/test_webcrypto.py index 2ed6405..fbffdc1 100644 --- a/packages/meshbay-common/tests/test_webcrypto.py +++ b/packages/meshbay-common/tests/test_webcrypto.py @@ -1,10 +1,11 @@ """Tests for AES-256-GCM webcrypto variant.""" import os -import pytest + import blake3 +import pytest from meshbay_common.crypto import generate_gek -from meshbay_common.webcrypto import chunk_key_aes, encrypt_chunk_aes, decrypt_chunk_aes +from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes, encrypt_chunk_aes def test_aes_roundtrip(): @@ -49,7 +50,10 @@ def test_aes_chunk_keys_unique_per_chunk(): def test_aes_gek_wrap_unwrap_roundtrip(): from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from meshbay_common.crypto import ( - wrap_gek_aes, unwrap_gek_aes, sk_to_raw, pk_to_raw, + pk_to_raw, + sk_to_raw, + unwrap_gek_aes, + wrap_gek_aes, ) gek = generate_gek() sk = X25519PrivateKey.generate() @@ -63,7 +67,7 @@ def test_aes_gek_wrap_unwrap_roundtrip(): def test_aes_gek_wrap_wrong_key_rejected(): from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import wrap_gek_aes, unwrap_gek_aes, sk_to_raw, pk_to_raw + from meshbay_common.crypto import pk_to_raw, sk_to_raw, unwrap_gek_aes, wrap_gek_aes gek = generate_gek() sk_a = X25519PrivateKey.generate() @@ -77,7 +81,9 @@ def test_aes_gek_wrap_wrong_key_rejected(): def test_aes_gek_wrap_differs_from_chacha_wrap(): from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from meshbay_common.crypto import ( - wrap_gek, wrap_gek_aes, pk_to_raw, + pk_to_raw, + wrap_gek, + wrap_gek_aes, ) gek = generate_gek() sk = X25519PrivateKey.generate() diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py index 7ca1e68..d8e13e9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py @@ -6,19 +6,18 @@ Separate from moderation.py (which handles public reporting and content blocklis """ import logging -from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_hub.auth import decrypt_email +from meshbay_hub import hub_settings from meshbay_hub.api.deps import require_admin, require_moderator, user_is_admin from meshbay_hub.api.revocation import get_connected_node_count, is_node_connected +from meshbay_hub.auth import decrypt_email from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User -from meshbay_hub import hub_settings log = logging.getLogger(__name__) diff --git a/packages/meshbay-hub/src/meshbay_hub/api/deps.py b/packages/meshbay-hub/src/meshbay_hub/api/deps.py index 907a481..501be9d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/deps.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/deps.py @@ -8,8 +8,8 @@ JWT scope enforcement: """ from fastapi import Depends, Header, HTTPException, status -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub.auth import decode_access_token from meshbay_hub.db.engine import get_db diff --git a/packages/meshbay-hub/src/meshbay_hub/api/federation.py b/packages/meshbay-hub/src/meshbay_hub/api/federation.py index 9e252c6..755639e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/federation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/federation.py @@ -23,18 +23,18 @@ Protocol version: MHP 0.1 import logging import time import uuid +from datetime import UTC import jwt -from fastapi import APIRouter, Depends, HTTPException, Header +from fastapi import APIRouter, Depends, Header, HTTPException +from meshbay_common import MHP_VERSION from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_common import MHP_VERSION from meshbay_hub import __version__, hub_settings from meshbay_hub.api.deps import require_admin -from meshbay_hub.auth import ( - hub_id, hub_private_key_pem, hub_public_key_pem) +from meshbay_hub.auth import hub_id, hub_private_key_pem, hub_public_key_pem from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import FederatedGroup, Group, HubPeer, User @@ -244,8 +244,8 @@ async def receive_directory( if len(body.groups) > MAX_FEDERATED_GROUPS_PER_PUSH: raise HTTPException(status_code=413, detail="Too many groups in one push") - from datetime import datetime, timezone - now = datetime.now(timezone.utc) + from datetime import datetime + now = datetime.now(UTC) have = await db.scalar( select(func.count()).select_from(FederatedGroup) .where(FederatedGroup.source_hub == sender)) or 0 diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py index 72ba194..3a11345 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py @@ -1,22 +1,27 @@ """Group endpoints — /v1/groups/*""" import re +from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel -from datetime import datetime, timezone from sqlalchemy import func, or_, select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub import hub_settings, mail -from meshbay_hub.auth import decrypt_email from meshbay_hub.api.deps import get_current_user, require_user_scope from meshbay_hub.api.middleware import limiter from meshbay_hub.api.netutil import client_ip +from meshbay_hub.auth import decrypt_email from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import ( - FederatedGroup, Group, GroupMember, IPLog, SwarmSource, User, + FederatedGroup, + Group, + GroupMember, + IPLog, + SwarmSource, + User, ) router = APIRouter(prefix="/v1/groups", tags=["groups"]) @@ -97,7 +102,7 @@ async def touch_group_activity( await db.execute( update(Group) .where(Group.id == group_id) - .values(last_activity_at=datetime.now(timezone.utc))) + .values(last_activity_at=datetime.now(UTC))) await db.commit() return {"ok": True} @@ -261,9 +266,9 @@ async def swarm_register( detail="endpoint must be ':' — a port on the " "registering node, not an address") - from datetime import datetime, timezone + from datetime import datetime existing = await db.get(SwarmSource, (body.content_hash, current_user.id)) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) if existing: existing.endpoint = body.endpoint existing.last_seen = now @@ -297,8 +302,8 @@ async def swarm_sources( Authenticated (H7): an open endpoint lets anyone probe whether a given file exists anywhere in the network and which node holds it. """ - from datetime import datetime, timezone, timedelta - cutoff = datetime.now(timezone.utc) - timedelta(minutes=30) + from datetime import datetime, timedelta + cutoff = datetime.now(UTC) - timedelta(minutes=30) result = await db.execute( select(SwarmSource) .where( diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py index 94e9b3c..cab60c8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py @@ -1,9 +1,9 @@ """Hub info endpoints — /v1/hub/*""" from fastapi import APIRouter, Depends +from meshbay_common import MHP_VERSION, MNP_VERSION from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_common import MNP_VERSION, MHP_VERSION from meshbay_hub import __version__, hub_settings from meshbay_hub.api import federation from meshbay_hub.auth import hub_public_key_pem diff --git a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py index ee10cbc..35c688c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py @@ -23,7 +23,6 @@ Node integration: """ import logging -from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py index 83b60f2..7478173 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py @@ -1,20 +1,20 @@ """Node endpoints — /v1/nodes/*""" -from datetime import datetime, timezone import base64 import time +from datetime import UTC, datetime -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_hub.auth import issue_access_token from meshbay_hub.api.deps import get_current_user from meshbay_hub.api.middleware import limiter from meshbay_hub.api.netutil import client_ip +from meshbay_hub.auth import issue_access_token from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import GroupMember, IPLog, Node, User @@ -156,7 +156,7 @@ async def announce_node( if node is not None: node.endpoint_hint = body.endpoint_hint node.observed_ip = seen_from - node.last_seen = datetime.now(timezone.utc) + node.last_seen = datetime.now(UTC) db.add(IPLog(user_id=current_user.id, event="node_announce", ip_address=seen_from, detail=body.endpoint_hint)) await db.commit() @@ -182,7 +182,7 @@ async def announce_node( pk_node=body.pk_node, endpoint_hint=body.endpoint_hint, observed_ip=seen_from, - last_seen=datetime.now(timezone.utc), + last_seen=datetime.now(UTC), ) db.add(node) db.add(IPLog( diff --git a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py index b5783ab..d96ec18 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py @@ -19,9 +19,9 @@ migration for no gain, and `unread_only` stays because it is what an older interface asks for and it still answers correctly — every row is unread. """ -from fastapi import APIRouter, Depends, HTTPException, Query -from datetime import datetime, timezone +from datetime import UTC, datetime +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -182,7 +182,7 @@ async def create_notification( existing.detail = detail existing.link = link existing.read = False - existing.created_at = datetime.now(timezone.utc) + existing.created_at = datetime.now(UTC) await db.flush() return existing diff --git a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py index 1f1f5c5..f8cae8a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py @@ -26,23 +26,21 @@ and close active connections for that user. """ import asyncio -import base64 import json import logging import time import uuid -from datetime import datetime, timezone -from typing import Any +from datetime import UTC, datetime +import jwt from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect +from meshbay_common.background import spawn from pydantic import BaseModel from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession -import jwt -from meshbay_common.background import spawn -from meshbay_hub.auth import hub_public_key_pem, decode_access_token from meshbay_hub.api.deps import get_current_user, require_admin +from meshbay_hub.auth import decode_access_token from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User @@ -143,7 +141,7 @@ async def _mark_hosted(group_ids: list[str]) -> None: await db.execute( update(Group) .where(Group.id.in_(group_ids), Group.hosted_at.is_(None)) - .values(hosted_at=datetime.now(timezone.utc))) + .values(hosted_at=datetime.now(UTC))) await db.commit() except Exception as e: # A group that stays unhosted in the table is visible to its owner and @@ -169,7 +167,7 @@ async def broadcast_revocation(token: str) -> int: def _sign_revocation(target: str, target_id: str, reason: str) -> str: """Issue a signed revocation token (JWT EdDSA).""" - from meshbay_hub.auth import _hub_sk_pem, _hub_id + from meshbay_hub.auth import _hub_id, _hub_sk_pem now = int(time.time()) payload = { "type": "revocation", @@ -208,9 +206,9 @@ async def _handle_chat_notify(group_id: str, sender_name: str, sender_user_id: s (node_id or "?")[:8]) return try: - from meshbay_hub.db.engine import get_session_factory - from meshbay_hub.db.models import GroupMember, Group from meshbay_hub.api.notifications import create_notification + from meshbay_hub.db.engine import get_session_factory + from meshbay_hub.db.models import Group, GroupMember async with get_session_factory()() as db: group = await db.get(Group, group_id) @@ -478,7 +476,7 @@ async def notify_incoming( try: await asyncio.wait_for(event.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: raise HTTPException(status_code=504, detail="Node did not respond in time") finally: _punch_events.pop(node_id, None) diff --git a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py index bc03b45..8a10822 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py @@ -25,8 +25,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub import hub_settings from meshbay_hub.api.deps import get_current_user from meshbay_hub.api.middleware import limiter -from meshbay_hub.db.engine import get_db from meshbay_hub.api.netutil import client_ip +from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import Group, GroupMember, IPLog, User log = logging.getLogger(__name__) @@ -167,7 +167,7 @@ async def webrtc_offer( try: answer = await asyncio.wait_for(answer_future, timeout=15.0) - except asyncio.TimeoutError: + except TimeoutError: raise HTTPException( status_code=504, detail="Node did not respond with WebRTC answer") diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index 0394f53..ee8aabc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -6,7 +6,7 @@ import re import secrets import time import uuid -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from fastapi import APIRouter, Depends, HTTPException, Request @@ -33,8 +33,17 @@ from meshbay_hub.auth import ( from meshbay_hub.config import HubConfig from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import ( - EmailVerification, Group, GroupMember, IPLog, Node, Notification, - RefreshToken, SwarmSource, User, UserDevice, UserPreference, + EmailVerification, + Group, + GroupMember, + IPLog, + Node, + Notification, + RefreshToken, + SwarmSource, + User, + UserDevice, + UserPreference, ) log = logging.getLogger(__name__) @@ -61,7 +70,7 @@ async def _refresh_expiry(db: AsyncSession, family_id: str | None = None) -> dat renewals of a tab that is being used. """ limits = await hub_settings.session_limits(db) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) idle = max(limits["refresh_idle_hours"] * 3600, _ttl() + 3600) started = now if family_id is not None: @@ -69,7 +78,7 @@ async def _refresh_expiry(db: AsyncSession, family_id: str | None = None) -> dat select(func.min(RefreshToken.created_at)) .where(RefreshToken.family_id == family_id)) if first is not None: - started = first if first.tzinfo else first.replace(tzinfo=timezone.utc) + started = first if first.tzinfo else first.replace(tzinfo=UTC) return min(now + timedelta(seconds=idle), started + timedelta(hours=limits["max_hours"])) @@ -193,7 +202,7 @@ async def register( EmailVerification.user_id == found.id, EmailVerification.purpose == "registration", EmailVerification.created_at - > datetime.now(timezone.utc) + > datetime.now(UTC) - timedelta(seconds=resend_cooldown), )) if not recent.first(): @@ -270,7 +279,7 @@ async def _create_and_send_verification( code=code, purpose="registration", user_id=user.id, - expires_at=datetime.now(timezone.utc) + timedelta(seconds=VERIFICATION_TTL), + expires_at=datetime.now(UTC) + timedelta(seconds=VERIFICATION_TTL), )) await db.flush() await mail.send_off_loop( @@ -292,7 +301,7 @@ async def verify_email( ): """Verify a registration email with the code received by mail.""" eh = hash_email_blind(body.email) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) result = await db.execute( select(EmailVerification).where( @@ -306,7 +315,7 @@ async def verify_email( raise HTTPException(status_code=404, detail="No pending verification for this email") - if verif.expires_at.replace(tzinfo=timezone.utc) < now: + if verif.expires_at.replace(tzinfo=UTC) < now: raise HTTPException(status_code=410, detail="Verification code expired") if verif.attempts >= VERIFICATION_MAX_ATTEMPTS: @@ -603,7 +612,7 @@ async def device_auth( await db.commit() raise HTTPException(status_code=401, detail="Invalid signature") - matched.last_seen = datetime.now(timezone.utc) + matched.last_seen = datetime.now(UTC) memberships = await db.execute( select(GroupMember.group_id).where(GroupMember.user_id == user.id)) @@ -650,7 +659,7 @@ async def token_refresh( await db.commit() raise HTTPException(status_code=401, detail="Token reuse detected — family revoked") - if rt.expires_at.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc): + if rt.expires_at.replace(tzinfo=UTC) < datetime.now(UTC): raise HTTPException(status_code=401, detail="Expired refresh token") user = await db.get(User, rt.user_id) @@ -659,7 +668,7 @@ async def token_refresh( # The family's first sign-in was longer ago than any session may last. expires_at = await _refresh_expiry(db, rt.family_id) - if expires_at <= datetime.now(timezone.utc): + if expires_at <= datetime.now(UTC): await db.execute( update(RefreshToken) .where(RefreshToken.family_id == rt.family_id) @@ -779,7 +788,7 @@ async def update_profile( cooldown = await hub_settings.get_int( db, "mail.email_change_cooldown", hub_settings.mail_default("email_change_cooldown")) - since = datetime.now(timezone.utc) - timedelta(seconds=cooldown) + since = datetime.now(UTC) - timedelta(seconds=cooldown) recent = await db.execute( select(IPLog).where( IPLog.user_id == current_user.id, @@ -820,7 +829,7 @@ async def update_profile( code=code, purpose="email_change", user_id=current_user.id, - expires_at=datetime.now(timezone.utc) + timedelta(seconds=VERIFICATION_TTL), + expires_at=datetime.now(UTC) + timedelta(seconds=VERIFICATION_TTL), )) db.add(IPLog(user_id=current_user.id, event="email_change_request", ip_address=client_ip(request))) @@ -860,7 +869,7 @@ async def verify_email_change( db: AsyncSession = Depends(get_db), ): """Confirm an email change with the code sent to the new address.""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) result = await db.execute( select(EmailVerification).where( @@ -874,7 +883,7 @@ async def verify_email_change( raise HTTPException(status_code=404, detail="No pending email change") - if verif.expires_at.replace(tzinfo=timezone.utc) < now: + if verif.expires_at.replace(tzinfo=UTC) < now: raise HTTPException(status_code=410, detail="Verification code expired") if verif.attempts >= VERIFICATION_MAX_ATTEMPTS: @@ -1090,7 +1099,7 @@ async def password_reset_request( EmailVerification.user_id == user.id, EmailVerification.purpose == "password_reset", EmailVerification.created_at - > datetime.now(timezone.utc) - timedelta(seconds=reset_cooldown), + > datetime.now(UTC) - timedelta(seconds=reset_cooldown), )) if recent.first(): return {"status": "sent_if_exists"} @@ -1110,7 +1119,7 @@ async def password_reset_request( code=code, purpose="password_reset", user_id=user.id, - expires_at=datetime.now(timezone.utc) + expires_at=datetime.now(UTC) + timedelta(seconds=PASSWORD_RESET_TTL), )) db.add(IPLog(user_id=user.id, event="password_reset_request", @@ -1141,7 +1150,7 @@ async def password_reset( request: Request, db: AsyncSession = Depends(get_db), ): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) result = await db.execute(select(User).where(User.username == body.username)) user = result.scalar_one_or_none() if not user: @@ -1157,7 +1166,7 @@ async def password_reset( if not verif: raise HTTPException(status_code=404, detail="No pending reset for this account") - if verif.expires_at.replace(tzinfo=timezone.utc) < now: + if verif.expires_at.replace(tzinfo=UTC) < now: raise HTTPException(status_code=410, detail="Reset code expired") if verif.attempts >= VERIFICATION_MAX_ATTEMPTS: raise HTTPException(status_code=429, detail="Too many attempts") diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py index afc1cde..209dc32 100644 --- a/packages/meshbay-hub/src/meshbay_hub/app.py +++ b/packages/meshbay-hub/src/meshbay_hub/app.py @@ -11,37 +11,41 @@ Usage: import asyncio from contextlib import asynccontextmanager -from pathlib import Path from fastapi import FastAPI from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from meshbay_hub import __version__ +from meshbay_hub.api.admin import router as admin_router +from meshbay_hub.api.deps import set_admin_usernames +from meshbay_hub.api.federation import router as federation_router +from meshbay_hub.api.groups import router as groups_router +from meshbay_hub.api.groups import swarm_router +from meshbay_hub.api.health import router as health_router +from meshbay_hub.api.hub import router as hub_router +from meshbay_hub.api.hub import set_config as hub_set_config +from meshbay_hub.api.middleware import limiter +from meshbay_hub.api.moderation import router as moderation_router +from meshbay_hub.api.nodes import router as nodes_router +from meshbay_hub.api.notifications import router as notifications_router +from meshbay_hub.api.relay import router as relay_router +from meshbay_hub.api.revocation import router as revocation_router +from meshbay_hub.api.signaling import router as signaling_router +from meshbay_hub.api.users import router as users_router +from meshbay_hub.api.users import set_config as users_set_config +from meshbay_hub.api.webapp import ASSET_V, CSP, STATIC_DIR +from meshbay_hub.api.webapp import router as webapp_router from meshbay_hub.auth import generate_hub_keypair, load_hub_keypair from meshbay_hub.config import HubConfig +from meshbay_hub.csam import csam_router from meshbay_hub.db.engine import close_db, init_db -from meshbay_hub.api.hub import router as hub_router, set_config as hub_set_config -from meshbay_hub.api.users import router as users_router, set_config as users_set_config -from meshbay_hub.api.deps import set_admin_usernames -from meshbay_hub.api.nodes import router as nodes_router -from meshbay_hub.api.groups import router as groups_router, swarm_router -from meshbay_hub.api.revocation import router as revocation_router -from meshbay_hub.api.moderation import router as moderation_router -from meshbay_hub.api.federation import router as federation_router -from meshbay_hub.csam import csam_router -from meshbay_hub.api.health import router as health_router -from meshbay_hub.api.relay import router as relay_router -from meshbay_hub.api.signaling import router as signaling_router -from meshbay_hub.api.admin import router as admin_router -from meshbay_hub.api.notifications import router as notifications_router -from meshbay_hub.api.webapp import router as webapp_router, STATIC_DIR, ASSET_V, CSP -from meshbay_hub.api.middleware import limiter async def _sync_admin_roles(admin_usernames: list[str]) -> None: """Ensure config-listed admin usernames have role='admin' in the DB.""" - from sqlalchemy import select, update + from sqlalchemy import select + from meshbay_hub.db.engine import get_session_factory from meshbay_hub.db.models import User @@ -122,8 +126,8 @@ def create_app(cfg: HubConfig | None = None) -> FastAPI: from meshbay_hub.csam import get_csam_checker get_csam_checker().load() - from meshbay_hub.tasks.cleanup import cleanup_loop from meshbay_hub.db.engine import get_session_factory + from meshbay_hub.tasks.cleanup import cleanup_loop cleanup_task = asyncio.create_task(cleanup_loop(get_session_factory())) yield diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index 7045197..d038027 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -18,12 +18,12 @@ from pathlib import Path import blake3 import jwt -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.kdf.argon2 import Argon2id from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives.hashes import SHA256 # Argon2id parameters — versioned for gradual migration _ARGON2_LANES = 4 diff --git a/packages/meshbay-hub/src/meshbay_hub/csam.py b/packages/meshbay-hub/src/meshbay_hub/csam.py index 2a0ce25..e540068 100644 --- a/packages/meshbay-hub/src/meshbay_hub/csam.py +++ b/packages/meshbay-hub/src/meshbay_hub/csam.py @@ -19,9 +19,7 @@ IMPORTANT: Never log matched hashes or file contents. CSAM detection must be reported to NCMEC (US law) or relevant authority immediately. """ -import hashlib import logging -import os from pathlib import Path log = logging.getLogger(__name__) @@ -123,7 +121,8 @@ def check_content_hash(blake3_hex: str) -> bool: # ── Hub API integration ─────────────────────────────────────────────────────── -from fastapi import APIRouter, Depends, HTTPException, UploadFile, File +from fastapi import APIRouter, Depends, HTTPException + from meshbay_hub.api.deps import require_admin from meshbay_hub.db.models import User diff --git a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py index 62e5388..9d7483a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py @@ -1,6 +1,6 @@ """Hub database layer.""" -from .engine import init_db, close_db, get_db -from .models import Base, User, Node, Group, GroupMember, RefreshToken, IPLog +from .engine import close_db, get_db, init_db +from .models import Base, Group, GroupMember, IPLog, Node, RefreshToken, User __all__ = [ "init_db", "close_db", "get_db", diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py index 8908deb..61f80be 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py @@ -4,14 +4,12 @@ import asyncio import os from logging.config import fileConfig +from alembic import context +from meshbay_hub.db.models import Base from sqlalchemy import pool from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import async_engine_from_config -from alembic import context - -from meshbay_hub.db.models import Base - config = context.config if config.config_file_name is not None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py index 8a4e2ee..034d12f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py @@ -5,16 +5,15 @@ Revises: f1a2b3c4d5e6 Create Date: 2026-08-20 20:50:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'a7b8c9d0e1f2' -down_revision: Union[str, Sequence[str], None] = 'f1a2b3c4d5e6' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'f1a2b3c4d5e6' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py index 2fead6c..45985e2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py @@ -7,15 +7,15 @@ Revision ID: a9b8c7d6e5f4 Revises: e5f6a7b8c9d0 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "a9b8c7d6e5f4" -down_revision: Union[str, Sequence[str], None] = "e5f6a7b8c9d0" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = "e5f6a7b8c9d0" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py index 13f2b5c..9a90f7a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py @@ -5,16 +5,15 @@ Revises: a7b8c9d0e1f2 Create Date: 2026-08-28 12:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'b1c2d3e4f5a6' -down_revision: Union[str, Sequence[str], None] = 'a7b8c9d0e1f2' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'a7b8c9d0e1f2' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py index 6fc5b73..fea0583 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py @@ -10,16 +10,15 @@ its UUID — this only makes `name@owner` a dependable handle. Pre-flight: abort if the data already violates it, with the offending (admin_id, name) pairs listed, rather than silently renaming anyone's group. """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'c3d4e5f6a7b8' -down_revision: Union[str, Sequence[str], None] = 'b1c2d3e4f5a6' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'b1c2d3e4f5a6' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py index 6301426..a814f37 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py @@ -12,16 +12,15 @@ Revises: Create Date: 2026-08-09 04:35:07.120021 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'd28b9caf9f07' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py index 6741eb8..aa15b6d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py @@ -9,16 +9,15 @@ Revises: c3d4e5f6a7b8 Create Date: 2026-08-31 14:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'd4e5f6a7b8c9' -down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'c3d4e5f6a7b8' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py index 8f2e4d2..c729de4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py @@ -8,15 +8,15 @@ Revision ID: e5f6a7b8c9d0 Revises: d4e5f6a7b8c9 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "e5f6a7b8c9d0" -down_revision: Union[str, Sequence[str], None] = "d4e5f6a7b8c9" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = "d4e5f6a7b8c9" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py index e30a4da..9f8e729 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py @@ -5,16 +5,15 @@ Revises: d28b9caf9f07 Create Date: 2026-08-19 12:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = 'f1a2b3c4d5e6' -down_revision: Union[str, Sequence[str], None] = 'd28b9caf9f07' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = 'd28b9caf9f07' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py index 62e4a11..38c4723 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/models.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py @@ -11,17 +11,23 @@ Tables: """ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import ( - Boolean, DateTime, ForeignKey, Index, Integer, - String, Text, UniqueConstraint, text, + Boolean, + DateTime, + ForeignKey, + Index, + Integer, + String, + Text, + text, ) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship def _now() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) def _uuid() -> str: return str(uuid.uuid4()) diff --git a/packages/meshbay-hub/src/meshbay_hub/login_throttle.py b/packages/meshbay-hub/src/meshbay_hub/login_throttle.py index 3281088..bfd142e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/login_throttle.py +++ b/packages/meshbay-hub/src/meshbay_hub/login_throttle.py @@ -24,7 +24,7 @@ locks somebody else's name from signing them out (§13.5b, AV26). """ import hashlib -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from sqlalchemy import case, delete, select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -39,7 +39,7 @@ def _key(username: str) -> str: def _aware(dt: datetime) -> datetime: # SQLite hands back naive datetimes for a timezone-aware column. - return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc) + return dt if dt.tzinfo is not None else dt.replace(tzinfo=UTC) def _insert_for(db: AsyncSession): @@ -64,7 +64,7 @@ async def reserve(db: AsyncSession, username: str) -> tuple[bool, int]: if max_failures == 0: return True, 0 - now = datetime.now(timezone.utc) + now = datetime.now(UTC) window = timedelta(minutes=limits["lockout_minutes"]) window_start = now - window key = _key(username) @@ -102,7 +102,7 @@ async def locked_for(db: AsyncSession, username: str) -> int: return 0 remaining = (_aware(row.last_failure_at) + timedelta(minutes=limits["lockout_minutes"]) - - datetime.now(timezone.utc)).total_seconds() + - datetime.now(UTC)).total_seconds() return max(0, int(remaining + 0.999)) @@ -136,7 +136,7 @@ async def clear(db: AsyncSession, username: str) -> None: async def purge_expired(db: AsyncSession) -> int: """Rows whose failures have aged out. Every unknown name typed creates one.""" limits = await hub_settings.login_limits(db) - cutoff = datetime.now(timezone.utc) - timedelta(minutes=limits["lockout_minutes"]) + cutoff = datetime.now(UTC) - timedelta(minutes=limits["lockout_minutes"]) result = await db.execute( delete(LoginThrottle).where(LoginThrottle.last_failure_at < cutoff)) await db.commit() diff --git a/packages/meshbay-hub/src/meshbay_hub/mail.py b/packages/meshbay-hub/src/meshbay_hub/mail.py index 126ed45..1eb7c51 100644 --- a/packages/meshbay-hub/src/meshbay_hub/mail.py +++ b/packages/meshbay-hub/src/meshbay_hub/mail.py @@ -16,7 +16,7 @@ import asyncio import hashlib import logging import smtplib -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from email.message import EmailMessage log = logging.getLogger(__name__) @@ -79,7 +79,7 @@ async def _take(db, key: str, window: timedelta, ceiling: int, """ from meshbay_hub.db.models import MailQuota - now = datetime.now(timezone.utc) + now = datetime.now(UTC) row = await db.get(MailQuota, key) if row is None: row = MailQuota(key=key, window_start=now, count=0, last_sent=None) @@ -87,14 +87,14 @@ async def _take(db, key: str, window: timedelta, ceiling: int, started = row.window_start if started.tzinfo is None: - started = started.replace(tzinfo=timezone.utc) + started = started.replace(tzinfo=UTC) if now - started >= window: row.window_start, row.count = now, 0 if cooldown is not None and row.last_sent is not None: last = row.last_sent if last.tzinfo is None: - last = last.replace(tzinfo=timezone.utc) + last = last.replace(tzinfo=UTC) if now - last < cooldown: raise MailRefused("too soon since the last message to this recipient") @@ -131,12 +131,12 @@ async def _announce_exhaustion(scope: str) -> None: try: async with get_session_factory()() as db: key = f"alert:{scope}" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) row = await db.get(MailQuota, key) if row is not None and row.last_sent is not None: last = row.last_sent if last.tzinfo is None: - last = last.replace(tzinfo=timezone.utc) + last = last.replace(tzinfo=UTC) if now - last < timedelta(hours=1): return if row is None: @@ -202,9 +202,10 @@ async def reserve(db, purpose: str, address: str) -> None: async def status(db) -> dict: """What the operator sees in the panel: is the hub still sending?""" + from sqlalchemy import func, select + from meshbay_hub import hub_settings from meshbay_hub.db.models import MailQuota - from sqlalchemy import func, select limits = await hub_settings.mail_limits(db) row = await db.get(MailQuota, "hour") @@ -213,8 +214,8 @@ async def status(db) -> dict: if row is not None: started = row.window_start if started.tzinfo is None: - started = started.replace(tzinfo=timezone.utc) - if datetime.now(timezone.utc) - started < timedelta(hours=1): + started = started.replace(tzinfo=UTC) + if datetime.now(UTC) - started < timedelta(hours=1): used, window_start = row.count, started.isoformat() recipients = await db.scalar( @@ -244,10 +245,11 @@ async def status(db) -> dict: async def purge_expired_quota(db) -> int: """Drop counters whose window has passed. Returns how many went.""" - from meshbay_hub.db.models import MailQuota from sqlalchemy import delete - cutoff = datetime.now(timezone.utc) - timedelta(days=1) + from meshbay_hub.db.models import MailQuota + + cutoff = datetime.now(UTC) - timedelta(days=1) result = await db.execute( delete(MailQuota).where(MailQuota.window_start < cutoff)) await db.commit() diff --git a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py index 5c52387..429575f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py +++ b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py @@ -2,7 +2,7 @@ import asyncio import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession @@ -16,7 +16,7 @@ CLEANUP_INTERVAL_HOURS = 24 async def purge_old_ip_logs(db: AsyncSession, retention_days: int = RETENTION_DAYS) -> int: - cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days) + cutoff = datetime.now(UTC) - timedelta(days=retention_days) result = await db.execute(delete(IPLog).where(IPLog.timestamp < cutoff)) await db.commit() return result.rowcount @@ -26,7 +26,7 @@ PENDING_USER_EXPIRY_DAYS = 7 async def purge_expired_verifications(db: AsyncSession) -> int: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) result = await db.execute( delete(EmailVerification).where(EmailVerification.expires_at < now)) await db.commit() @@ -35,7 +35,7 @@ async def purge_expired_verifications(db: AsyncSession) -> int: async def purge_stale_pending_users(db: AsyncSession, expiry_days: int = PENDING_USER_EXPIRY_DAYS) -> int: - cutoff = datetime.now(timezone.utc) - timedelta(days=expiry_days) + cutoff = datetime.now(UTC) - timedelta(days=expiry_days) result = await db.execute( delete(User).where(User.status == "pending", User.created_at < cutoff)) await db.commit() @@ -93,7 +93,7 @@ async def find_unhosted_groups(db: AsyncSession, grace_days: int = UNHOSTED_GRAC whole reason the column exists rather than a check against the live socket registry, which would delete every group during a hub restart. """ - cutoff = datetime.now(timezone.utc) - timedelta(days=grace_days) + cutoff = datetime.now(UTC) - timedelta(days=grace_days) result = await db.execute( select(Group).where(Group.hosted_at.is_(None), Group.created_at < cutoff)) return list(result.scalars().all()) diff --git a/packages/meshbay-hub/tests/conftest.py b/packages/meshbay-hub/tests/conftest.py index bf94464..bb0ed28 100644 --- a/packages/meshbay-hub/tests/conftest.py +++ b/packages/meshbay-hub/tests/conftest.py @@ -1,12 +1,12 @@ """Shared pytest fixtures for hub tests.""" import os +from pathlib import Path + import pytest import pytest_asyncio -from pathlib import Path -from unittest.mock import AsyncMock -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey # Force SQLite in-memory for all hub tests os.environ.setdefault("MESHBAY_DATABASE_URL", "sqlite+aiosqlite:///:memory:") @@ -28,7 +28,13 @@ def hub_key_path(tmp_path_factory) -> Path: @pytest.fixture def hub_config(hub_key_path, tmp_path): - from meshbay_hub.config import HubConfig, DatabaseConfig, ServerConfig, HubIdentityConfig, JWTConfig + from meshbay_hub.config import ( + DatabaseConfig, + HubConfig, + HubIdentityConfig, + JWTConfig, + ServerConfig, + ) cfg = HubConfig( db=DatabaseConfig(url="sqlite+aiosqlite:///:memory:"), server=ServerConfig(host="127.0.0.1", port=8000), diff --git a/packages/meshbay-hub/tests/harness/chat_send_probe.py b/packages/meshbay-hub/tests/harness/chat_send_probe.py index b6dd957..d343c50 100644 --- a/packages/meshbay-hub/tests/harness/chat_send_probe.py +++ b/packages/meshbay-hub/tests/harness/chat_send_probe.py @@ -96,7 +96,6 @@ def _page() -> str: the page built itself would prove only that the page agrees with the page. """ import msgpack # noqa: F401 (imported for the failure it gives if absent) - from meshbay_common.groupbox import PURPOSE_CHAT_KEYS, seal sealed = seal(GEK, PURPOSE_CHAT_KEYS, "chat_keys_resp", GROUP_ID, diff --git a/packages/meshbay-hub/tests/test_account_deletion.py b/packages/meshbay-hub/tests/test_account_deletion.py index 3d3fee4..2653f0d 100644 --- a/packages/meshbay-hub/tests/test_account_deletion.py +++ b/packages/meshbay-hub/tests/test_account_deletion.py @@ -12,9 +12,8 @@ command. import hashlib import pytest -from sqlalchemy import select - from meshbay_hub.db.models import GroupMember, Notification, RefreshToken, User +from sqlalchemy import select def _auth_key(password: str, username: str) -> str: diff --git a/packages/meshbay-hub/tests/test_account_pinning.py b/packages/meshbay-hub/tests/test_account_pinning.py index 192bd25..a1419c3 100644 --- a/packages/meshbay-hub/tests/test_account_pinning.py +++ b/packages/meshbay-hub/tests/test_account_pinning.py @@ -23,7 +23,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.device import device_add_transcript STATIC = (Path(__file__).resolve().parents[1] diff --git a/packages/meshbay-hub/tests/test_admin_views.py b/packages/meshbay-hub/tests/test_admin_views.py index 5017b76..da2d3c9 100644 --- a/packages/meshbay-hub/tests/test_admin_views.py +++ b/packages/meshbay-hub/tests/test_admin_views.py @@ -11,9 +11,8 @@ import base64 import hashlib import pytest -from sqlalchemy import select - from meshbay_hub.db.models import User +from sqlalchemy import select def _auth_key(password: str, username: str) -> str: @@ -109,9 +108,8 @@ async def test_a_node_is_recorded_at_the_address_it_announced_from( import base64 import time - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization - + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_hub.db.models import Node admin = await _admin(client, db_session, "root4_test") diff --git a/packages/meshbay-hub/tests/test_asset_versioning.py b/packages/meshbay-hub/tests/test_asset_versioning.py index 0c93f5a..aa2dc6d 100644 --- a/packages/meshbay-hub/tests/test_asset_versioning.py +++ b/packages/meshbay-hub/tests/test_asset_versioning.py @@ -22,7 +22,6 @@ import re import pytest from fastapi.testclient import TestClient - from meshbay_hub.api.webapp import ASSET_V, STATIC_DIR, _asset_version from meshbay_hub.app import create_app diff --git a/packages/meshbay-hub/tests/test_availability_between_members.py b/packages/meshbay-hub/tests/test_availability_between_members.py index 2be7c03..4f5cbfb 100644 --- a/packages/meshbay-hub/tests/test_availability_between_members.py +++ b/packages/meshbay-hub/tests/test_availability_between_members.py @@ -24,7 +24,6 @@ import time import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import pk_to_b64 @@ -158,8 +157,7 @@ async def test_one_node_cannot_spend_the_hubs_database_on_notifications(): without backpressure. The budget is what stops one group's node from costing every other group on the instance. """ - from meshbay_hub.api.revocation import ( - NOTIFY_BURST, _notify_budget, _notify_window) + from meshbay_hub.api.revocation import NOTIFY_BURST, _notify_budget, _notify_window node = "budget-node" _notify_window.pop(node, None) @@ -265,8 +263,7 @@ def test_a_node_cannot_answer_an_offer_it_was_never_sent(): """ import asyncio - from meshbay_hub.api.signaling import ( - _answer_owner, _webrtc_answers, handle_webrtc_answer) + from meshbay_hub.api.signaling import _answer_owner, _webrtc_answers, handle_webrtc_answer loop = asyncio.new_event_loop() try: @@ -657,6 +654,7 @@ async def test_a_member_still_reaches_the_node_they_share_a_group_with(client): class _AnsweringWS: async def send_text(self, text): import json as _json + from meshbay_hub.api.signaling import handle_webrtc_answer msg = _json.loads(text) answered.append(msg) diff --git a/packages/meshbay-hub/tests/test_captcha_host_check.py b/packages/meshbay-hub/tests/test_captcha_host_check.py index 68cad34..778009f 100644 --- a/packages/meshbay-hub/tests/test_captcha_host_check.py +++ b/packages/meshbay-hub/tests/test_captcha_host_check.py @@ -31,12 +31,10 @@ TOML list is a typo far more often than an intention. import json import pytest - from meshbay_hub.captcha import verify_captcha from meshbay_hub.config import CaptchaConfig, load_config - class _Resp: def __init__(self, payload): self._payload = payload diff --git a/packages/meshbay-hub/tests/test_device_auth.py b/packages/meshbay-hub/tests/test_device_auth.py index e899f12..f9b172e 100644 --- a/packages/meshbay-hub/tests/test_device_auth.py +++ b/packages/meshbay-hub/tests/test_device_auth.py @@ -21,9 +21,7 @@ stopped being true. import base64 import time -import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from meshbay_common.crypto import pk_to_b64 diff --git a/packages/meshbay-hub/tests/test_group_hosting.py b/packages/meshbay-hub/tests/test_group_hosting.py index c571771..c6111f2 100644 --- a/packages/meshbay-hub/tests/test_group_hosting.py +++ b/packages/meshbay-hub/tests/test_group_hosting.py @@ -14,13 +14,12 @@ deleted every group during a hub restart. import base64 import hashlib -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest -from sqlalchemy import select - from meshbay_hub.db.models import Group, GroupMember from meshbay_hub.tasks.cleanup import find_unhosted_groups, prune_unhosted_groups +from sqlalchemy import select def _auth_key(password: str, username: str) -> str: @@ -49,7 +48,7 @@ async def _group(client, owner, name, visibility="private", join_policy=None): async def _mark_hosted(db_session, group_id, when=None): g = await db_session.get(Group, group_id) - g.hosted_at = when or datetime.now(timezone.utc) + g.hosted_at = when or datetime.now(UTC) await db_session.commit() @@ -134,7 +133,7 @@ async def test_an_unhosted_group_past_the_grace_period_is_collected(client, db_s gid = (await _group(client, owner, "abandoned")).json()["group_id"] g = await db_session.get(Group, gid) - g.created_at = datetime.now(timezone.utc) - timedelta(days=8) + g.created_at = datetime.now(UTC) - timedelta(days=8) await db_session.commit() gone = await prune_unhosted_groups(db_session) @@ -149,8 +148,8 @@ async def test_an_old_group_that_was_hosted_is_never_collected(client, db_sessio gid = (await _group(client, owner, "long-lived")).json()["group_id"] g = await db_session.get(Group, gid) - g.created_at = datetime.now(timezone.utc) - timedelta(days=400) - g.hosted_at = datetime.now(timezone.utc) - timedelta(days=399) + g.created_at = datetime.now(UTC) - timedelta(days=400) + g.hosted_at = datetime.now(UTC) - timedelta(days=399) await db_session.commit() assert await find_unhosted_groups(db_session) == [] @@ -161,7 +160,7 @@ async def test_dry_run_reports_without_deleting(client, db_session): owner = await _user(client, "reaper4_test") gid = (await _group(client, owner, "still-here")).json()["group_id"] g = await db_session.get(Group, gid) - g.created_at = datetime.now(timezone.utc) - timedelta(days=30) + g.created_at = datetime.now(UTC) - timedelta(days=30) await db_session.commit() gone = await prune_unhosted_groups(db_session, dry_run=True) @@ -178,7 +177,7 @@ async def test_collecting_a_group_takes_its_memberships_with_it(client, db_sessi await client.post(f"/v1/groups/{gid}/members/tagalong", json={}, headers=owner) g = await db_session.get(Group, gid) - g.created_at = datetime.now(timezone.utc) - timedelta(days=9) + g.created_at = datetime.now(UTC) - timedelta(days=9) await db_session.commit() await prune_unhosted_groups(db_session) @@ -247,7 +246,7 @@ async def test_the_stamp_is_not_moved_by_a_later_reconnection(client, db_session owner = await _user(client, "stamped2") gid = (await _group(client, owner, "steady")).json()["group_id"] - first = datetime.now(timezone.utc) - timedelta(days=30) + first = datetime.now(UTC) - timedelta(days=30) await _mark_hosted([gid]) g = await db_session.get(Group, gid) g.hosted_at = first @@ -259,7 +258,7 @@ async def test_the_stamp_is_not_moved_by_a_later_reconnection(client, db_session await db_session.refresh(g) # SQLite hands back a naive datetime where PostgreSQL keeps the offset, so # the comparison is made on common ground rather than on the driver. - stored = g.hosted_at.replace(tzinfo=timezone.utc) if g.hosted_at.tzinfo is None \ + stored = g.hosted_at.replace(tzinfo=UTC) if g.hosted_at.tzinfo is None \ else g.hosted_at assert abs((stored - first).total_seconds()) < 1, "the stamp moved" diff --git a/packages/meshbay-hub/tests/test_group_leave_and_quota.py b/packages/meshbay-hub/tests/test_group_leave_and_quota.py index 4431f53..8af830d 100644 --- a/packages/meshbay-hub/tests/test_group_leave_and_quota.py +++ b/packages/meshbay-hub/tests/test_group_leave_and_quota.py @@ -14,13 +14,12 @@ removes them, and whoever left still holds the group key they were served. import base64 import hashlib -from datetime import datetime, timezone +from datetime import UTC, datetime import pytest -from sqlalchemy import select - from meshbay_hub.api.groups import MAX_PUBLIC_GROUPS from meshbay_hub.db.models import Group, GroupMember, User +from sqlalchemy import select def _auth_key(password: str, username: str) -> str: @@ -61,7 +60,7 @@ async def test_a_member_can_leave(client, db_session): # Marked hosted, or the member would not see the group in the first place # and the assertion below would hold whether or not leaving worked. g = await db_session.get(Group, gid) - g.hosted_at = datetime.now(timezone.utc) + g.hosted_at = datetime.now(UTC) await db_session.commit() before = await client.get("/v1/groups/mine", headers=member) diff --git a/packages/meshbay-hub/tests/test_group_membership.py b/packages/meshbay-hub/tests/test_group_membership.py index 2761595..ad1b3ab 100644 --- a/packages/meshbay-hub/tests/test_group_membership.py +++ b/packages/meshbay-hub/tests/test_group_membership.py @@ -11,10 +11,9 @@ import base64 import hashlib import pytest +from meshbay_hub.db.models import GroupMember, User from sqlalchemy import select -from meshbay_hub.db.models import Group, GroupMember, User - def _auth_key(password: str, username: str) -> str: salt = hashlib.sha256(f"meshbay:auth:v1:{username}".encode()).digest() diff --git a/packages/meshbay-hub/tests/test_group_purge.py b/packages/meshbay-hub/tests/test_group_purge.py index 0611dba..e729297 100644 --- a/packages/meshbay-hub/tests/test_group_purge.py +++ b/packages/meshbay-hub/tests/test_group_purge.py @@ -21,13 +21,18 @@ from datetime import UTC, datetime, timedelta import jwt import pytest -from sqlalchemy import delete, func, select, text -from sqlalchemy.exc import IntegrityError - from meshbay_hub.db.models import ( - ContentReport, EmailVerification, Group, GroupMember, IPLog, Notification, User, + ContentReport, + EmailVerification, + Group, + GroupMember, + IPLog, + Notification, + User, ) from meshbay_hub.db.purge import _referencing +from sqlalchemy import delete, func, select, text +from sqlalchemy.exc import IntegrityError SEEDED = {"group_members", "notifications", "email_verifications", "content_reports"} diff --git a/packages/meshbay-hub/tests/test_groups_self_service.py b/packages/meshbay-hub/tests/test_groups_self_service.py index f9346c1..a3a8f2d 100644 --- a/packages/meshbay-hub/tests/test_groups_self_service.py +++ b/packages/meshbay-hub/tests/test_groups_self_service.py @@ -1,9 +1,10 @@ """Integration tests for group self-service: create, join, members.""" +from datetime import UTC + import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import pk_to_b64 @@ -43,10 +44,11 @@ async def _create_group(client, token, name="test-group", visibility="public", async def _mark_hosted(db_session, *group_ids): """Pretend a node announced these groups, as /v1/nodes/ws would.""" - from datetime import datetime, timezone + from datetime import datetime + from meshbay_hub.db.models import Group for gid in group_ids: - (await db_session.get(Group, gid)).hosted_at = datetime.now(timezone.utc) + (await db_session.get(Group, gid)).hosted_at = datetime.now(UTC) await db_session.commit() diff --git a/packages/meshbay-hub/tests/test_hub_api.py b/packages/meshbay-hub/tests/test_hub_api.py index 37e8e4f..9b73701 100644 --- a/packages/meshbay-hub/tests/test_hub_api.py +++ b/packages/meshbay-hub/tests/test_hub_api.py @@ -3,13 +3,11 @@ Integration tests for the Hub API. Uses SQLite in-memory + httpx.AsyncClient — no PostgreSQL, no network. """ -import base64 +from datetime import UTC + import pytest -import pytest_asyncio from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey -from cryptography.hazmat.primitives import serialization - from meshbay_common.crypto import pk_to_b64 from meshbay_hub.api.deps import set_admin_usernames @@ -32,7 +30,8 @@ async def _announce_signed(client, token: str) -> tuple[str, str]: The node key is independent of the user's identity key, so this mints a fresh one and signs the domain-separated announce message with it. """ - import base64 as _b64, time as _t + import base64 as _b64 + import time as _t me = await client.get("/v1/users/me", headers={"Authorization": f"Bearer {token}"}) @@ -399,9 +398,10 @@ async def test_my_groups(client, db_session): # A group no node has announced is shown to its owner only — a member would # otherwise see a name they cannot open. Stamped here so the rest of this # test is about membership, which is what it was written for. - from datetime import datetime, timezone + from datetime import datetime + from meshbay_hub.db.models import Group - (await db_session.get(Group, group_id)).hosted_at = datetime.now(timezone.utc) + (await db_session.get(Group, group_id)).hosted_at = datetime.now(UTC) await db_session.commit() # Re-login to get fresh token with group claims @@ -434,7 +434,6 @@ async def test_my_groups(client, db_session): @pytest.mark.asyncio async def test_group_online_nodes(client): """GET /v1/groups/{id}/nodes returns online nodes serving the group.""" - import json from meshbay_hub.api.revocation import _connected_nodes, _node_groups pk_ed, pk_x, _ = _gen_user_keys() @@ -541,7 +540,7 @@ async def test_admin_can_revoke(client): @pytest.mark.asyncio async def test_email_encrypted_at_rest(client): """Email stored in DB must not contain plaintext address.""" - from meshbay_hub.auth import encrypt_email, decrypt_email + from meshbay_hub.auth import decrypt_email, encrypt_email encrypted = encrypt_email("test@example.com") assert "@" not in encrypted assert decrypt_email(encrypted) == "test@example.com" @@ -589,9 +588,10 @@ async def test_password_rehash_on_login(client, app): user = result.scalar_one() user.pw_version = 1 # Re-hash with v1 params so verify_password(version=1) succeeds - from meshbay_hub.auth import _ARGON2_VERSIONS, _ARGON2_KEY_LEN, _ARGON2_LANES - from cryptography.hazmat.primitives.kdf.argon2 import Argon2id import os + + from cryptography.hazmat.primitives.kdf.argon2 import Argon2id + from meshbay_hub.auth import _ARGON2_KEY_LEN, _ARGON2_LANES, _ARGON2_VERSIONS salt = os.urandom(16) params = _ARGON2_VERSIONS[1] pw_hash = Argon2id( @@ -760,13 +760,14 @@ async def test_webrtc_signaling_roundtrip(client, app): @pytest.mark.asyncio async def test_ip_log_cleanup(app): """Old IP log entries are purged by cleanup task.""" - from datetime import datetime, timezone, timedelta + from datetime import datetime, timedelta + from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import IPLog from meshbay_hub.tasks.cleanup import purge_old_ip_logs async for db in get_db(): - old_ts = datetime.now(timezone.utc) - timedelta(days=400) + old_ts = datetime.now(UTC) - timedelta(days=400) db.add(IPLog(event="test_old", ip_address="1.2.3.4", timestamp=old_ts)) db.add(IPLog(event="test_recent", ip_address="5.6.7.8")) await db.commit() @@ -774,7 +775,7 @@ async def test_ip_log_cleanup(app): deleted = await purge_old_ip_logs(db, retention_days=365) assert deleted == 1 - from sqlalchemy import select, func + from sqlalchemy import func, select count = (await db.execute( select(func.count()).where(IPLog.event.in_(["test_old", "test_recent"])) )).scalar_one() diff --git a/packages/meshbay-hub/tests/test_login_lockout.py b/packages/meshbay-hub/tests/test_login_lockout.py index 6f17d98..601edbd 100644 --- a/packages/meshbay-hub/tests/test_login_lockout.py +++ b/packages/meshbay-hub/tests/test_login_lockout.py @@ -18,14 +18,13 @@ is `test_availability_between_members.py`, because it takes two accounts. """ import asyncio -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest -from sqlalchemy import select, update - from meshbay_hub.api.deps import set_admin_usernames from meshbay_hub.db.models import LoginThrottle from meshbay_hub.login_throttle import _key +from sqlalchemy import select, update RIGHT = "r" * 44 WRONG = "w" * 44 @@ -94,7 +93,7 @@ async def test_a_lockout_ends_when_its_window_does(client, db_session): await db_session.execute( update(LoginThrottle).where(LoginThrottle.key == _key("dave_test")) - .values(last_failure_at=datetime.now(timezone.utc) - timedelta(minutes=61))) + .values(last_failure_at=datetime.now(UTC) - timedelta(minutes=61))) await db_session.commit() assert (await _login(client, "dave_test", RIGHT)).status_code == 200 @@ -106,7 +105,7 @@ async def test_old_failures_do_not_carry_into_a_new_window(client, db_session): await _fail(client, "erin_test", 3) await db_session.execute( update(LoginThrottle).where(LoginThrottle.key == _key("erin_test")) - .values(last_failure_at=datetime.now(timezone.utc) - timedelta(minutes=61))) + .values(last_failure_at=datetime.now(UTC) - timedelta(minutes=61))) await db_session.commit() # One stale window of three, then one fresh failure: a count of one, not four. diff --git a/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py b/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py index fe6a5ee..a3f9a21 100644 --- a/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py +++ b/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py @@ -28,7 +28,6 @@ import base64 import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import pk_to_b64 from meshbay_hub import mail as mail_mod from meshbay_hub.api import users as users_mod @@ -316,9 +315,8 @@ async def _signed_in(client, db_session, username: str, email: str) -> dict: genuinely `pending` here, exactly as it would be in production, and a pending account cannot log in. """ - from sqlalchemy import select, update - from meshbay_hub.db.models import User + from sqlalchemy import select, update r = await _register(client, username, email) assert r.status_code == 201, r.text @@ -406,9 +404,8 @@ async def test_the_delay_survives_the_verification_row_being_deleted( json={"email": "c-first@example.test"}) assert r.status_code == 200, r.text - from sqlalchemy import delete - from meshbay_hub.db.models import EmailVerification + from sqlalchemy import delete await db_session.execute(delete(EmailVerification)) await db_session.commit() @@ -421,9 +418,8 @@ async def test_the_delay_survives_the_verification_row_being_deleted( # ── The operator's controls ────────────────────────────────────────────────── async def _admin(client, db_session, username: str) -> dict: - from sqlalchemy import update - from meshbay_hub.db.models import User + from sqlalchemy import update headers = await _signed_in(client, db_session, username, f"{username}@example.test") @@ -491,9 +487,8 @@ async def test_an_unknown_mail_setting_is_refused(client, db_session): @pytest.mark.asyncio async def test_a_moderator_may_read_the_bounds_but_not_change_them( client, db_session): - from sqlalchemy import update - from meshbay_hub.db.models import User + from sqlalchemy import update headers = await _signed_in(client, db_session, "mailmod_test", "mailmod@example.test") diff --git a/packages/meshbay-hub/tests/test_migrations_reach_head.py b/packages/meshbay-hub/tests/test_migrations_reach_head.py index 13c5590..7f0d5df 100644 --- a/packages/meshbay-hub/tests/test_migrations_reach_head.py +++ b/packages/meshbay-hub/tests/test_migrations_reach_head.py @@ -30,9 +30,8 @@ from pathlib import Path import pytest from alembic import command from alembic.config import Config -from sqlalchemy import create_engine, inspect - from meshbay_hub.db.models import Base +from sqlalchemy import create_engine, inspect HUB = Path(__file__).resolve().parents[1] diff --git a/packages/meshbay-hub/tests/test_node_auth.py b/packages/meshbay-hub/tests/test_node_auth.py index 4137932..09816de 100644 --- a/packages/meshbay-hub/tests/test_node_auth.py +++ b/packages/meshbay-hub/tests/test_node_auth.py @@ -8,9 +8,9 @@ import base64 import time 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 def _gen_ed25519(): diff --git a/packages/meshbay-hub/tests/test_node_ws_auth.py b/packages/meshbay-hub/tests/test_node_ws_auth.py index f3ac3a2..7d96851 100644 --- a/packages/meshbay-hub/tests/test_node_ws_auth.py +++ b/packages/meshbay-hub/tests/test_node_ws_auth.py @@ -16,7 +16,6 @@ import base64 import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import pk_to_b64 @@ -405,7 +404,10 @@ async def test_empty_node_cannot_shadow_another_members_group(client): source for that group, because it is the one clients would reach first. """ from meshbay_hub.api.revocation import ( - _authorize_node_ws, _node_groups, get_online_nodes_for_group) + _authorize_node_ws, + _node_groups, + get_online_nodes_for_group, + ) host = await _make_user(client, "hoster_test") guest = await _make_user(client, "guest_test") diff --git a/packages/meshbay-hub/tests/test_notification_dismissal.py b/packages/meshbay-hub/tests/test_notification_dismissal.py index 1c9c7b3..d498b4d 100644 --- a/packages/meshbay-hub/tests/test_notification_dismissal.py +++ b/packages/meshbay-hub/tests/test_notification_dismissal.py @@ -32,7 +32,6 @@ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import pk_to_b64 from meshbay_hub.api.deps import set_admin_usernames diff --git a/packages/meshbay-hub/tests/test_notifications.py b/packages/meshbay-hub/tests/test_notifications.py index 02aca1c..b87e1d5 100644 --- a/packages/meshbay-hub/tests/test_notifications.py +++ b/packages/meshbay-hub/tests/test_notifications.py @@ -3,7 +3,6 @@ import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import pk_to_b64 from meshbay_hub.api.deps import set_admin_usernames diff --git a/packages/meshbay-hub/tests/test_notifications_behaviour.py b/packages/meshbay-hub/tests/test_notifications_behaviour.py index a247508..67fd99e 100644 --- a/packages/meshbay-hub/tests/test_notifications_behaviour.py +++ b/packages/meshbay-hub/tests/test_notifications_behaviour.py @@ -7,13 +7,12 @@ browser's localStorage and nothing read it), and there was no way to clear the list. """ -import hashlib import base64 +import hashlib import pytest -from sqlalchemy import select - from meshbay_hub.db.models import GroupMember, Notification, User +from sqlalchemy import select def _auth_key(password: str, username: str) -> str: diff --git a/packages/meshbay-hub/tests/test_packaging_hub_unit.py b/packages/meshbay-hub/tests/test_packaging_hub_unit.py index 7d8f5d2..e29164e 100644 --- a/packages/meshbay-hub/tests/test_packaging_hub_unit.py +++ b/packages/meshbay-hub/tests/test_packaging_hub_unit.py @@ -85,7 +85,6 @@ def test_the_command_resolves_from_the_installed_package_not_the_checkout(): """Derived from `meshbay_hub.__file__`, so it is correct in a venv, an RPM and a checkout alike — which is the whole point of not writing it down.""" import meshbay_hub - from meshbay_hub.daemon import migrations_dir assert migrations_dir() == ( diff --git a/packages/meshbay-hub/tests/test_password_change.py b/packages/meshbay-hub/tests/test_password_change.py index b2fe586..bfe0c97 100644 --- a/packages/meshbay-hub/tests/test_password_change.py +++ b/packages/meshbay-hub/tests/test_password_change.py @@ -11,9 +11,8 @@ import base64 import hashlib import pytest -from sqlalchemy import select - from meshbay_hub.db.models import IPLog, RefreshToken, User +from sqlalchemy import select def _auth_key(password: str, username: str) -> str: diff --git a/packages/meshbay-hub/tests/test_password_reset.py b/packages/meshbay-hub/tests/test_password_reset.py index 823807c..b07f77c 100644 --- a/packages/meshbay-hub/tests/test_password_reset.py +++ b/packages/meshbay-hub/tests/test_password_reset.py @@ -9,7 +9,7 @@ the recovery key and is not exercised here. import base64 import time -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey @@ -126,7 +126,7 @@ async def test_an_expired_code_is_refused(client, db_session): select(User.id).where(User.username == "carol_test"))).scalar_one() row = (await db_session.execute(select(EmailVerification).where( EmailVerification.user_id == uid))).scalars().one() - row.expires_at = datetime.now(timezone.utc) - timedelta(minutes=1) + row.expires_at = datetime.now(UTC) - timedelta(minutes=1) await db_session.commit() r = await client.post("/v1/users/password/reset", json={ diff --git a/packages/meshbay-hub/tests/test_public_groups_toggle.py b/packages/meshbay-hub/tests/test_public_groups_toggle.py index 96e3383..dbc6a38 100644 --- a/packages/meshbay-hub/tests/test_public_groups_toggle.py +++ b/packages/meshbay-hub/tests/test_public_groups_toggle.py @@ -17,7 +17,7 @@ and their access — plan A, not a purge. import base64 import hashlib -from datetime import datetime, timezone +from datetime import UTC, datetime import pytest from meshbay_hub.api.deps import set_admin_usernames @@ -61,7 +61,7 @@ async def _create_public(client, owner, name): async def _mark_hosted(db_session, *group_ids): """Pretend a node announced these groups, as /v1/nodes/ws would.""" for gid in group_ids: - (await db_session.get(Group, gid)).hosted_at = datetime.now(timezone.utc) + (await db_session.get(Group, gid)).hosted_at = datetime.now(UTC) await db_session.commit() diff --git a/packages/meshbay-hub/tests/test_revocation.py b/packages/meshbay-hub/tests/test_revocation.py index ad9caf4..e2ca5e7 100644 --- a/packages/meshbay-hub/tests/test_revocation.py +++ b/packages/meshbay-hub/tests/test_revocation.py @@ -1,11 +1,9 @@ """Tests for revocation — admin endpoint + token signing.""" -import time -import pytest import jwt +import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey - from meshbay_common.crypto import pk_to_b64 from meshbay_hub.api.deps import set_admin_usernames diff --git a/packages/meshbay-hub/tests/test_session_lifetime.py b/packages/meshbay-hub/tests/test_session_lifetime.py index af73767..3a1f47a 100644 --- a/packages/meshbay-hub/tests/test_session_lifetime.py +++ b/packages/meshbay-hub/tests/test_session_lifetime.py @@ -6,13 +6,12 @@ maximum — plus a sign-out the hub honours and "sign out everywhere". The browser half is `test_browser_idle_signout.py`; this is the hub's. """ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest -from sqlalchemy import select, update - from meshbay_hub.api.deps import set_admin_usernames from meshbay_hub.db.models import RefreshToken, User +from sqlalchemy import select, update AUTH_KEY = "s" * 44 @@ -40,7 +39,7 @@ async def _refresh(client, token): def _aware(dt): - return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc) + return dt if dt.tzinfo else dt.replace(tzinfo=UTC) async def _expiry(db_session, username): @@ -66,7 +65,7 @@ async def test_the_defaults_are_in_the_panel_and_the_browser_delay_is_public(cli async def test_a_refresh_token_lasts_the_idle_window(client, db_session): await _register(client, "idle_window") await _login(client, "idle_window") - left = await _expiry(db_session, "idle_window") - datetime.now(timezone.utc) + left = await _expiry(db_session, "idle_window") - datetime.now(UTC) assert timedelta(hours=23, minutes=58) < left <= timedelta(hours=24) @@ -78,7 +77,7 @@ async def test_the_idle_window_never_undercuts_the_access_token(client, db_sessi json={"session": {"refresh_idle_hours": 1}}) await _register(client, "short_idle") await _login(client, "short_idle") - left = await _expiry(db_session, "short_idle") - datetime.now(timezone.utc) + left = await _expiry(db_session, "short_idle") - datetime.now(UTC) # The test hub's access token lives 3600 s; the floor is that plus an hour. assert left > timedelta(hours=1, minutes=58) @@ -98,7 +97,7 @@ async def test_no_session_renews_past_its_maximum(client, db_session): select(User.id).where(User.username == "long_session"))).scalar_one() await db_session.execute( update(RefreshToken).where(RefreshToken.user_id == uid) - .values(created_at=datetime.now(timezone.utc) - timedelta(hours=721))) + .values(created_at=datetime.now(UTC) - timedelta(hours=721))) await db_session.commit() r = await _refresh(client, current) diff --git a/packages/meshbay-hub/tests/test_session_renewal.py b/packages/meshbay-hub/tests/test_session_renewal.py index 5d4bcfd..7b8c108 100644 --- a/packages/meshbay-hub/tests/test_session_renewal.py +++ b/packages/meshbay-hub/tests/test_session_renewal.py @@ -26,9 +26,9 @@ broken client. """ import json +import re import shutil import subprocess -import re from pathlib import Path import pytest diff --git a/packages/meshbay-hub/tests/test_table_rows_measured.py b/packages/meshbay-hub/tests/test_table_rows_measured.py index 60ec878..f203624 100644 --- a/packages/meshbay-hub/tests/test_table_rows_measured.py +++ b/packages/meshbay-hub/tests/test_table_rows_measured.py @@ -16,11 +16,10 @@ rectangles show it does not — which is what this file is for. """ import json +import shutil import subprocess from pathlib import Path -import shutil - import pytest HARNESS = Path(__file__).parent / "harness" / "layout_probe.py" 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) diff --git a/poc/spike1_crypto.py b/poc/spike1_crypto.py index 335758c..1e8491c 100644 --- a/poc/spike1_crypto.py +++ b/poc/spike1_crypto.py @@ -4,17 +4,20 @@ MeshBay — Spike 1: Crypto Primitives Validates the full cryptographic stack needed for MeshBay. """ -import os, time, base64, sys +import base64 +import os +import sys +import time +import blake3 +import jwt +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.kdf.argon2 import Argon2id -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -import blake3 -import jwt +from cryptography.hazmat.primitives.kdf.hkdf import HKDF PASS = "✓" FAIL = "✗" diff --git a/poc/spike2_hub.py b/poc/spike2_hub.py index f8c1b08..a0ebd6f 100644 --- a/poc/spike2_hub.py +++ b/poc/spike2_hub.py @@ -4,10 +4,15 @@ Minimal FastAPI hub: user registration, JWT issuance, node announcement. In-memory storage only — not persistent across restarts. """ -from fastapi import FastAPI, HTTPException, Depends, Header -from pydantic import BaseModel +import base64 +import os +import time +import uuid + +import jwt from cryptography.hazmat.primitives.kdf.argon2 import Argon2id -import jwt, uuid, os, time, base64 +from fastapi import Depends, FastAPI, Header, HTTPException +from pydantic import BaseModel app = FastAPI(title="MeshBay Hub POC", version="0.1.0") diff --git a/poc/spike3_node.py b/poc/spike3_node.py index c0ddde0..0b6d694 100644 --- a/poc/spike3_node.py +++ b/poc/spike3_node.py @@ -12,12 +12,17 @@ Sequence: 8. Simulate token refresh """ -import asyncio, httpx, base64, json, os, time +import asyncio +import base64 +import json +import time from pathlib import Path + +import httpx +import jwt +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 -import jwt HUB_URL = "http://meshbay.org" STATE_FILE = Path("node_state.json") # persists keys and tokens between runs @@ -52,7 +57,7 @@ def _load_or_generate_keys(state: dict) -> tuple: sk_x = X25519PrivateKey.generate() state["sk_ed25519_b64"] = _sk_to_b64(sk_ed) state["sk_x25519_b64"] = _sk_to_b64(sk_x) - print(f" Generated new keypair") + print(" Generated new keypair") return sk_ed, sk_x # ── Main ────────────────────────────────────────────────────────────────────── @@ -101,7 +106,7 @@ async def main(): state["user_id"] = r.json()["user_id"] print(f" {PASS} Registered — user_id={state['user_id'][:8]}...") elif r.status_code == 409: - print(f" Already registered (409) — continuing with existing account") + print(" Already registered (409) — continuing with existing account") else: raise RuntimeError(f"Register failed: {r.status_code} {r.text}") @@ -149,7 +154,7 @@ async def main(): node_id = r.json()["node_id"] state["node_id"] = node_id print(f" {PASS} Node announced — node_id={node_id[:8]}...") - print(f" endpoint_hint : None (STUN/UPnP discovery in Spike 4)") + print(" endpoint_hint : None (STUN/UPnP discovery in Spike 4)") # ── Step 7: Retrieve node record ────────────────────────────────────── print("\n[ 7 ] Retrieve node record from hub") diff --git a/poc/spike4_nat.py b/poc/spike4_nat.py index 44bcdca..e34a4c4 100644 --- a/poc/spike4_nat.py +++ b/poc/spike4_nat.py @@ -12,9 +12,15 @@ Tests: E. Update hub endpoint_hint """ -import asyncio, socket, struct, os, json, httpx, time, subprocess +import asyncio +import json +import os +import socket +import struct from pathlib import Path +import httpx + PASS = "✓"; FAIL = "✗"; SKIP = "–" LOCAL_PORT = 19000 @@ -148,7 +154,7 @@ async def udp_hole_punch_test(local_port: int) -> tuple[bool, str|None, str|None try: await asyncio.wait_for(recv_event.wait(), timeout=8) - except asyncio.TimeoutError: + except TimeoutError: pass finally: transport.close() @@ -192,7 +198,7 @@ async def main(): print(f" {FAIL} {nat_type}") print(f" Cloudflare STUN : {stun_ip1}:{stun_port1}") print(f" Google STUN : {stun_ip2}:{stun_port2}") - print(f" → Hole punching unreliable; TURN relay required") + print(" → Hole punching unreliable; TURN relay required") elif stun_ip1: nat_type = "Unknown (only one STUN server responded)" print(f" {SKIP} {nat_type}") @@ -202,7 +208,7 @@ async def main(): print(f" {FAIL} {nat_type}") # ── Phase C: Bidirectional UDP hole punching ─────────────────────────────── - print(f"\n[ C ] Bidirectional UDP hole punch (node → meshbay.org → echo back)") + print("\n[ C ] Bidirectional UDP hole punch (node → meshbay.org → echo back)") print(f" Starting UDP echo server on meshbay.org:{MESHBAY_ECHO_PORT} ...") udp_ok = False seen_ext_addr = None @@ -234,7 +240,7 @@ async def main(): print(f" {FAIL} Error: {e}") # ── Phase D: UPnP ───────────────────────────────────────────────────────── - print(f"\n[ D ] UPnP port mapping") + print("\n[ D ] UPnP port mapping") upnp_ip, upnp_port = try_upnp(LOCAL_PORT) if upnp_ip: print(f" {PASS} Mapped {upnp_ip}:{upnp_port}") @@ -243,7 +249,7 @@ async def main(): print(f" {FAIL} UPnP not available on this router") # ── Phase E: Update hub ──────────────────────────────────────────────────── - print(f"\n[ E ] Update endpoint_hint on hub") + print("\n[ E ] Update endpoint_hint on hub") if endpoint_hint and state.get("username"): async with httpx.AsyncClient(timeout=10) as client: r = await client.post(f"{HUB_URL}/v1/users/login", json={ @@ -270,10 +276,10 @@ async def main(): print(f" UPnP : {PASS + ' works' if upnp_ip else FAIL + ' disabled'}") print(f" endpoint_hint : {endpoint_hint or 'none'}") if udp_ok: - print(f"\n P2P UDP is functional. QUIC transport will work.") - print(f" Spike 4 COMPLETE.") + print("\n P2P UDP is functional. QUIC transport will work.") + print(" Spike 4 COMPLETE.") else: - print(f"\n P2P blocked — TURN relay needed for this configuration.") - print(f" Spike 4 COMPLETE (with finding: relay required).") + print("\n P2P blocked — TURN relay needed for this configuration.") + print(" Spike 4 COMPLETE (with finding: relay required).") asyncio.run(main()) diff --git a/poc/spike5_client.py b/poc/spike5_client.py index 11b3ac0..6ac2fba 100644 --- a/poc/spike5_client.py +++ b/poc/spike5_client.py @@ -12,12 +12,18 @@ Acts as the "file requester": - Reports timing and results """ -import asyncio, json, base64, struct, time, sys +import asyncio +import base64 +import json +import struct +import sys +import time + +import blake3 +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives import hashes -import blake3 PORT = 19003 CHUNK_INDEX = 0 @@ -115,7 +121,7 @@ async def handle_node(reader, writer): total_ms = recv_ms + verify_ms print(f"\n {'='*50}") - print(f" Timings:") + print(" Timings:") print(f" Network receive : {recv_ms:.0f} ms") print(f" Verify+decrypt : {verify_ms:.1f} ms") print(f" Total : {total_ms:.0f} ms") @@ -133,7 +139,7 @@ async def main(): async with server: try: await asyncio.wait_for(server.serve_forever(), timeout=30) - except asyncio.TimeoutError: + except TimeoutError: print(f" {FAIL} Timeout — node did not connect within 30s") sys.exit(1) diff --git a/poc/spike5_node.py b/poc/spike5_node.py index 6104e00..b560dc1 100644 --- a/poc/spike5_node.py +++ b/poc/spike5_node.py @@ -14,13 +14,19 @@ Note: node initiates the TCP connection (reversed for POC — in production, the QUIC client connects to the node using the hole-punching from Spike 4). """ -import asyncio, json, base64, struct, os, time +import asyncio +import base64 +import json +import os +import struct +import time from pathlib import Path + +import blake3 +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives import hashes, serialization -import blake3 MESHBAY_HOST = "meshbay.org" MESHBAY_PORT = 19003 @@ -125,7 +131,7 @@ async def main(): print(f" {PASS} Test file: {TEST_FILE} ({file_size/1024/1024:.1f} MB)") # Pre-compute file hash (used in HKDF info string — identifies the file) - print(f" Computing file hash...") + print(" Computing file hash...") t0 = time.perf_counter() file_hash = blake3.blake3(TEST_FILE.read_bytes()).digest() print(f" {PASS} File hash: {file_hash.hex()[:16]}... ({(time.perf_counter()-t0)*1000:.0f}ms)") @@ -166,7 +172,7 @@ async def main(): print(f" {PASS} Sent {payload_kb:.1f} KB in {send_ms:.0f}ms ({throughput:.1f} MB/s)") print(f"\n {'='*50}") - print(f" Timings (node side):") + print(" Timings (node side):") print(f" Encrypt + sign : {enc_ms:.1f} ms") print(f" TCP send : {send_ms:.0f} ms") print(f" Total : {total_ms:.0f} ms") diff --git a/poc/spike6_gek.py b/poc/spike6_gek.py index 89ab06c..3595d6d 100644 --- a/poc/spike6_gek.py +++ b/poc/spike6_gek.py @@ -16,14 +16,20 @@ Validates the full GEK lifecycle: The hub stores opaque encrypted blobs — it never sees the GEK in cleartext. """ -import asyncio, httpx, base64, os, json, time +import asyncio +import base64 +import json +import os +import time from pathlib import Path + +import blake3 +import httpx +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives import hashes, serialization -import blake3 HUB_URL = "http://meshbay.org" STATE_FILE = Path("node_state.json") @@ -218,7 +224,7 @@ async def main(): wrap_ms = (time.perf_counter()-t0)*1000 print(f" {PASS} Wrapped in {wrap_ms:.2f}ms") print(f" pk_eph : {bundle_bob['pk_eph_b64'][:24]}...") - print(f" (different from Alice's bundle — ephemeral key is unique)") + print(" (different from Alice's bundle — ephemeral key is unique)") r = await c.post( f"{HUB_URL}/v1/groups/{group_id}/members/bob_member/gek", @@ -301,10 +307,10 @@ async def main(): print(f" {PASS} Wrong private key correctly rejected (AEAD auth failed)") print("\n" + "="*55) - print(f"Spike 6 COMPLETE — GEK distribution validated.") + print("Spike 6 COMPLETE — GEK distribution validated.") print(f" wrap_gek : {wrap_ms:.2f} ms") print(f" unwrap_gek : {unwrap_ms:.2f} ms") - print(f" Hub role : stores opaque bundle, never sees GEK in clear") - print(f" Security : wrong key rejected by AEAD authentication tag") + print(" Hub role : stores opaque bundle, never sees GEK in clear") + print(" Security : wrong key rejected by AEAD authentication tag") asyncio.run(main()) -- cgit v1.2.3