summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/config.py4
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py42
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/__init__.py4
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/enrich.py6
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py4
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/group_index.py19
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/indexer.py11
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/title_parse.py5
-rw-r--r--packages/meshbay-node/src/meshbay_node/keystore.py6
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py10
-rw-r--r--packages/meshbay-node/src/meshbay_node/replication.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/revocation.py155
-rw-r--r--packages/meshbay-node/src/meshbay_node/roots.py4
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py12
-rw-r--r--packages/meshbay-node/src/meshbay_node/transfers.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/__init__.py4
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/quic_client.py7
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/quic_server.py11
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/tls_cert.py9
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py132
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/uploads.py2
23 files changed, 154 insertions, 301 deletions
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..bb50bcf 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():
@@ -2244,7 +2243,8 @@ def main() -> None:
_GUIDANCE = {
"node_key_link": (
"Link node key",
- f"Copy the node key above and paste it in Settings → Link Node on {cfg.hub.url}"),
+ f"Copy the node key above and paste it in "
+ f"Settings → Link Node on {cfg.hub.url}"),
"group_add": (
"Add a group",
"meshbay-node group add <name> --dir /path/to/files"),
@@ -2475,7 +2475,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..91bf2bd 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
@@ -218,7 +218,8 @@ class ParsedName:
year: int | None = None
season: int | None = None
episode: int | None = None
- confidence: bool = False # True only when display_title is set and structurally corroborated
+ # True only when display_title is set and structurally corroborated
+ confidence: bool = False
def parse_movie_filename(filename: str) -> ParsedName:
diff --git a/packages/meshbay-node/src/meshbay_node/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
deleted file mode 100644
index d3ee18f..0000000
--- a/packages/meshbay-node/src/meshbay_node/revocation.py
+++ /dev/null
@@ -1,155 +0,0 @@
-"""
-MeshBay Node — revocation subscriber.
-
-Maintains a persistent WebSocket connection to the hub.
-When a signed revocation token arrives, verifies it (Ed25519)
-and adds the revoked target to the local blocklist.
-
-Usage in daemon:
- subscriber = RevocationSubscriber(hub_url, access_token, hub_pk_pem)
- await subscriber.start() # connects in background
- # check membership:
- if subscriber.is_revoked("user", user_id):
- refuse connection
- await subscriber.stop()
-"""
-
-import asyncio
-import json
-import logging
-import time
-from typing import Literal
-
-import httpx
-import jwt
-
-log = logging.getLogger(__name__)
-
-RevocationTarget = Literal["user", "group"]
-
-
-class RevocationSubscriber:
- """
- Background task that keeps a WebSocket connection to the hub
- and maintains a local revocation set.
- """
-
- def __init__(
- self,
- hub_url: str,
- access_token: str,
- hub_pk_pem: bytes,
- reconnect_delay: float = 5.0,
- ):
- self._hub_url = hub_url.rstrip("/")
- self._access_token = access_token
- self._hub_pk_pem = hub_pk_pem
- self._reconnect_delay = reconnect_delay
- self._revoked_users: set[str] = set()
- self._revoked_groups: set[str] = set()
- self._task: asyncio.Task | None = None
- self._running = False
-
- def is_revoked(self, target: RevocationTarget, target_id: str) -> bool:
- if target == "user":
- return target_id in self._revoked_users
- return target_id in self._revoked_groups
-
- def add_revocation(self, target: RevocationTarget, target_id: str) -> None:
- if target == "user":
- self._revoked_users.add(target_id)
- log.warning("User revoked locally: %s", target_id[:8])
- else:
- self._revoked_groups.add(target_id)
- log.warning("Group revoked locally: %s", target_id[:8])
-
- def verify_and_apply(self, token: str) -> bool:
- """Verify a revocation token and apply it. Returns True if valid."""
- try:
- from meshbay_common.handshake import JWT_LEEWAY_SECONDS
- payload = jwt.decode(token, self._hub_pk_pem, algorithms=["EdDSA"],
- leeway=JWT_LEEWAY_SECONDS,
- options={"verify_exp": False})
- if payload.get("type") != "revocation":
- return False
- target = payload["target"]
- target_id = payload["target_id"]
- self.add_revocation(target, target_id)
- return True
- except Exception as e:
- log.error("Invalid revocation token: %s", e)
- return False
-
- async def start(self) -> None:
- self._running = True
- self._task = asyncio.create_task(self._run_loop())
- log.info("RevocationSubscriber started")
-
- async def stop(self) -> None:
- self._running = False
- if self._task:
- self._task.cancel()
- try:
- await self._task
- except asyncio.CancelledError:
- pass
- log.info("RevocationSubscriber stopped")
-
- async def _run_loop(self) -> None:
- while self._running:
- try:
- await self._connect_and_listen()
- except asyncio.CancelledError:
- raise
- except Exception as e:
- log.warning("WS disconnected (%s), reconnecting in %ss", e, self._reconnect_delay)
- await asyncio.sleep(self._reconnect_delay)
-
- async def _connect_and_listen(self) -> None:
- ws_url = self._hub_url.replace("http://", "ws://").replace("https://", "wss://")
- ws_url += "/v1/nodes/ws"
-
- async with httpx.AsyncClient() as client:
- async with client.stream("GET", ws_url,
- headers={"Upgrade": "websocket"}) as resp:
- # Use websockets library for proper WS protocol
- pass
-
- # Use websockets library directly
- import websockets
- async with websockets.connect(ws_url) as ws:
- # Authenticate
- await ws.send(json.dumps({
- "type": "auth",
- "token": self._access_token,
- }))
- auth_resp = json.loads(await ws.recv())
- if auth_resp.get("type") != "auth_ok":
- raise ConnectionError(f"WS auth failed: {auth_resp}")
- log.info("WS connected to hub — node_id=%s", auth_resp.get("node_id", "?")[:8])
-
- # Listen for revocations + send keepalive pings
- ping_interval = 30.0
- last_ping = asyncio.get_event_loop().time()
-
- while self._running:
- now = asyncio.get_event_loop().time()
- if now - last_ping > ping_interval:
- await ws.send(json.dumps({"type": "ping"}))
- last_ping = now
-
- try:
- msg_raw = await asyncio.wait_for(ws.recv(), timeout=ping_interval + 5)
- msg = json.loads(msg_raw)
-
- if msg.get("type") == "revocation":
- token = msg.get("token", "")
- ok = self.verify_and_apply(token)
- log.info("Revocation received — valid=%s", ok)
- elif msg.get("type") == "pong":
- pass
- else:
- log.debug("WS message: %s", msg.get("type"))
-
- except asyncio.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/transfers.py b/packages/meshbay-node/src/meshbay_node/transfers.py
index 86c8d14..3345cb9 100644
--- a/packages/meshbay-node/src/meshbay_node/transfers.py
+++ b/packages/meshbay-node/src/meshbay_node/transfers.py
@@ -393,7 +393,7 @@ class TransferSlots:
# the one place it would be tempting to add one for a prettier
# log line.
for x in sorted(self.leases.values(),
- key=lambda l: (l.kind, l.state, l.created_at))
+ key=lambda ls: (ls.kind, ls.state, ls.created_at))
],
}
diff --git a/packages/meshbay-node/src/meshbay_node/transport/__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..e92ec13 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__)
@@ -3924,7 +3930,8 @@ class WebRTCPeerSession:
if not entry:
thumb = await self._try_serve_thumbnail(file_id, chunk_index, ctx.get("gek"))
if thumb is not None:
- log.debug("file_req file_id=%s chunk=%s: served as thumbnail", file_id[:16], chunk_index)
+ log.debug("file_req file_id=%s chunk=%s: served as thumbnail",
+ file_id[:16], chunk_index)
self._send(thumb)
return
log.warning("File not found: %s", file_id[:16])
@@ -3992,7 +3999,8 @@ class WebRTCPeerSession:
self._leaseless.finish(str(file_id))
@staticmethod
- async def _fetch_and_cache_poster(media_cache, tmdb_client, poster_path: str | None) -> str | None:
+ async def _fetch_and_cache_poster(media_cache, tmdb_client,
+ poster_path: str | None) -> str | None:
"""
Downloads a TMDB poster/backdrop once, caches it under its own
blake3 like a video thumbnail (docs/MESHBAY_DESIGN.md §9.7), and
@@ -4020,7 +4028,8 @@ class WebRTCPeerSession:
return thumb_hash
@staticmethod
- async def _fetch_and_cache_cover(media_cache, musicbrainz_client, mbid: str | None) -> str | None:
+ async def _fetch_and_cache_cover(media_cache, musicbrainz_client,
+ mbid: str | None) -> str | None:
"""
Music app equivalent of `_fetch_and_cache_poster` — a release's
Cover Art Archive image, fetched once per mbid and cached under its
@@ -4504,7 +4513,9 @@ class WebRTCPeerSession:
# itself.
if not fetched.get("overview"):
fallback = await tmdb_client.tv_season(tmdb_id, season, language="en-US") or {}
- fetched = {**fallback, **{k: v for k, v in fetched.items() if v not in (None, "", [])}}
+ fetched = {**fallback,
+ **{k: v for k, v in fetched.items()
+ if v not in (None, "", [])}}
await media_cache.set_season_meta(tmdb_id, season, fetched)
details = fetched
@@ -4833,8 +4844,10 @@ class WebRTCPeerSession:
# back to English per field rather than discarding an otherwise-good
# localized response over one empty one — mirrored here the same
# way, at field granularity, not by abandoning the whole response.
- if not details.get("overview") or not details.get("poster_path") or not details.get("genres"):
- fallback = (await tmdb_client.tv_details(tmdb_id, language="en-US") if media_type == "tv"
+ if (not details.get("overview") or not details.get("poster_path")
+ or not details.get("genres")):
+ fallback = (await tmdb_client.tv_details(tmdb_id, language="en-US")
+ if media_type == "tv"
else await tmdb_client.movie_details(tmdb_id, language="en-US")) or {}
details = {**fallback, **{k: v for k, v in details.items() if v not in (None, "", [])}}
credits = (await tmdb_client.tv_credits(tmdb_id) if media_type == "tv"
@@ -5909,7 +5922,8 @@ class WebRTCPeerSession:
transcript = admin_transcript(
op=pending["op"],
node_pk_b64=self._node_pk_b64(),
- group_id=pending["group_id"] if pending.get("group_id") is not None else (self._group_id or ""),
+ group_id=(pending["group_id"] if pending.get("group_id") is not None
+ else (self._group_id or "")),
subject=pending["subject"],
nonce=pending["nonce"],
ts=pending["ts"],
@@ -6160,7 +6174,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 +6222,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 +6865,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 +6926,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 +6981,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 +7166,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