diff --git a/README.md b/README.md index d3554c1..f0fe459 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,53 @@ Qubes are AI agents you genuinely own. Each Qube has a cryptographic identity mi │ Category: c9054d53dcc075dd7226ea319f20d43d │ │ f102371149311c9239f6c0ea1200b80f │ └─────────────────────────────────────────────┘ + + (parallel stack — no message data ever on-chain) +┌─────────────────────────────────────────────┐ +│ Relay Layer │ +│ Qubes P2P Relay Protocol │ +│ Kademlia DHT · Noise encryption │ +│ 2-hop onion routing · Store-and-forward │ +│ Nostr fallback (Phase 3) │ +└───────────────┬─────────────────────────────┘ + │ transport-agnostic +┌───────────────▼─────────────────────────────┐ +│ Transport Drivers (updatable bundle) │ +│ TCP/WS · QUIC · BLE · LoRa · I2P │ +└─────────────────────────────────────────────┘ +``` + +The desktop app is one implementation of the protocol. The SDK is the protocol itself. The relay layer is a completely separate stack — no message content, metadata, or communication graph ever touches the BCH blockchain. + +--- + +## P2P Relay + +Qubes includes a private peer-to-peer messaging relay for cross-Qube communication. It is transport-agnostic, metadata-private, and requires no central server. + +Every Qubes desktop installation automatically runs a lightweight relay node. Nodes find each other through a Kademlia DHT seeded by a built-in list of community relays — the same pattern Electron Cash uses for Fulcrum servers. Messages route through 2-hop onion encryption (Phase 2) so relay operators cannot determine who is communicating with whom. + +When a recipient is offline, their nearest DHT neighbors hold the encrypted message for up to 7 days. Messages are purged immediately after delivery. Conversation history is stored locally inside the Qube's signed memory chain, not on relays. + +**Transport fallback waterfall** (automatic, no user action required): + +``` +Priority 1 Local relay User's own desktop relay node +Priority 2 Internet DHT Kademlia routing, Noise-encrypted +Priority 3 Nostr fallback Phase 3 — ephemeral keys + NIP-44 encryption +Priority 4 LoRa / BLE mesh Phase 6 — extreme offline, no internet needed +``` + +**Running a dedicated relay node** (for community relay operators): + +```bash +cd relay +npm run relay:start # production, port 4001 +npm run relay:dev # verbose logging +npm run relay:start -- --port 4001 --max-connections 200 --retention-days 7 ``` -The desktop app is one implementation of the protocol. The SDK is the protocol itself. +Every Qubes desktop user already contributes relay capacity just by running the app. Dedicated relay nodes are for always-on community infrastructure, like running a BCH full node. --- @@ -56,7 +100,7 @@ The desktop app is one implementation of the protocol. The SDK is the protocol i npm install @qubesai/sdk ``` -**8 modules:** `types` · `crypto` · `wallet` · `blocks` · `covenant` · `package` · `bcmr` · `storage` +**10 modules:** `types` · `crypto` · `wallet` · `blocks` · `covenant` · `package` · `bcmr` · `storage` · `relay` · `nostr` ### Quick example — generate an identity diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e2be695 --- /dev/null +++ b/__init__.py @@ -0,0 +1,66 @@ +# mypy: allow-untyped-defs +from torch.nn.parameter import ( # usort: skip + Buffer as Buffer, + Parameter as Parameter, + UninitializedBuffer as UninitializedBuffer, + UninitializedParameter as UninitializedParameter, +) +# Pre-import functional and init so submodules can do `import torch.nn.functional as F` +# without hitting a circular import during torch.nn initialization +import torch.nn.functional # noqa: E402 +import torch.nn.init # noqa: E402 +from torch.nn.modules import * # usort: skip # noqa: F403 +from torch.nn import ( + attention as attention, + functional as functional, + init as init, + modules as modules, + parallel as parallel, + parameter as parameter, + utils as utils, +) +from torch.nn.parallel import DataParallel as DataParallel + + +def factory_kwargs(kwargs): + r"""Return a canonicalized dict of factory kwargs. + + Given kwargs, returns a canonicalized dict of factory kwargs that can be directly passed + to factory functions like torch.empty, or errors if unrecognized kwargs are present. + + This function makes it simple to write code like this:: + + class MyModule(nn.Module): + def __init__(self, **kwargs): + factory_kwargs = torch.nn.factory_kwargs(kwargs) + self.weight = Parameter(torch.empty(10, **factory_kwargs)) + + Why should you use this function instead of just passing `kwargs` along directly? + + 1. This function does error validation, so if there are unexpected kwargs we will + immediately report an error, instead of deferring it to the factory call + 2. This function supports a special `factory_kwargs` argument, which can be used to + explicitly specify a kwarg to be used for factory functions, in the event one of the + factory kwargs conflicts with an already existing argument in the signature (e.g. + in the signature ``def f(dtype, **kwargs)``, you can specify ``dtype`` for factory + functions, as distinct from the dtype argument, by saying + ``f(dtype1, factory_kwargs={"dtype": dtype2})``) + """ + if kwargs is None: + return {} + simple_keys = {"device", "dtype", "memory_format"} + expected_keys = simple_keys | {"factory_kwargs"} + if not kwargs.keys() <= expected_keys: + raise TypeError(f"unexpected kwargs {kwargs.keys() - expected_keys}") + + # guarantee no input kwargs is untouched + r = dict(kwargs.get("factory_kwargs", {})) + for k in simple_keys: + if k in kwargs: + if k in r: + raise TypeError( + f"{k} specified twice, in **kwargs and in factory_kwargs" + ) + r[k] = kwargs[k] + + return r diff --git a/audio/audio_manager.py b/audio/audio_manager.py index a7f85d2..72073cb 100644 --- a/audio/audio_manager.py +++ b/audio/audio_manager.py @@ -207,6 +207,8 @@ def __init__(self, config: Optional[Dict[str, Any]] = None, qube_data_dir: Optio def _load_config_from_env(self) -> Dict[str, Any]: """Load configuration from environment variables""" + # Resolve base models dir: QUBES_MODELS_DIR env var or platform default + _models_base = Path(os.getenv("QUBES_MODELS_DIR", "~/.qubes/models")).expanduser() return { "openai_api_key": os.getenv("OPENAI_API_KEY"), "elevenlabs_api_key": os.getenv("ELEVENLABS_API_KEY"), @@ -216,15 +218,16 @@ def _load_config_from_env(self) -> Dict[str, Any]: "piper_model_path": Path( os.getenv( "PIPER_MODEL_PATH", - "~/.qubes/models/piper/en_US-lessac-medium.onnx" + str(_models_base / "piper" / "en_US-lessac-medium.onnx") ) ), "whisper_cpp_model_path": Path( os.getenv( "WHISPER_MODEL_PATH", - "~/.qubes/models/whisper/ggml-base.en.bin" + str(_models_base / "whisper" / "ggml-base.en.bin") ) ), + "qwen3_models_dir": _models_base / "qwen3-tts", } def _init_tts_providers(self): @@ -384,9 +387,28 @@ def _get_qwen3_provider(self) -> Optional[TTSProvider]: except Exception: pass # Use defaults + # Use QUBES_MODELS_DIR if set, otherwise platform default + qwen3_models_dir = self.config.get("qwen3_models_dir") + + # Auto-download model if not present + if qwen3_models_dir is not None: + try: + from audio.model_downloader import Qwen3ModelDownloader + _dl = Qwen3ModelDownloader(models_dir=qwen3_models_dir) + _variant_key = f"{model_variant}-Base" + if not _dl.is_model_downloaded(_variant_key): + logger.info("qwen3_auto_download_triggered", variant=_variant_key) + _dl.start_download(_variant_key) + # Also download tokenizer + if not _dl.is_model_downloaded("Tokenizer"): + _dl.start_download("Tokenizer") + except Exception as _dl_err: + logger.warning("qwen3_auto_download_failed", error=str(_dl_err)) + provider = Qwen3TTSProvider( model_variant=model_variant, - use_flash_attention=use_flash_attention + use_flash_attention=use_flash_attention, + **({"models_dir": qwen3_models_dir} if qwen3_models_dir else {}) ) self.tts_providers["qwen3"] = provider logger.info("tts_provider_initialized", provider="qwen3") @@ -489,7 +511,8 @@ def check_qwen3_status(self) -> Dict[str, Any]: result["available"] = result["recommended_variant"] is not None # Check downloaded models - models_dir = Path.home() / ".qubes" / "models" / "qwen3-tts" + _models_base = Path(os.getenv("QUBES_MODELS_DIR", str(Path.home() / ".qubes" / "models"))) + models_dir = _models_base / "qwen3-tts" if models_dir.exists(): for model_dir in models_dir.iterdir(): if model_dir.is_dir(): diff --git a/config/user_preferences.py b/config/user_preferences.py index 8229c79..c632415 100644 --- a/config/user_preferences.py +++ b/config/user_preferences.py @@ -29,9 +29,9 @@ class BlockPreferences: group_anchor_threshold: int = 20 # Anchor threshold for group chats # IPFS backup settings - auto_sync_ipfs_on_anchor: bool = False # Auto-sync to IPFS after auto-anchor - auto_sync_ipfs_periodic: bool = False # Periodic background sync to IPFS - auto_sync_ipfs_interval: int = 15 # Interval in minutes (15, 30, 45, 60) + auto_sync_ipfs_on_anchor: bool = True # Auto-sync to IPFS after auto-anchor + auto_sync_ipfs_periodic: bool = True # Periodic background sync to IPFS + auto_sync_ipfs_interval: int = 15 # Interval in minutes (5, 15, 30, 60) @dataclass @@ -94,6 +94,42 @@ class Qwen3Preferences: ]) +@dataclass +class RelayPreferences: + """Preferences for the P2P relay node.""" + + relay_enabled: bool = True + relay_listen_port: int = 0 # 0 = auto-assign + relay_max_connections: int = 50 + relay_retention_days: int = 7 + relay_custom_peers: List[str] = field(default_factory=list) + p2pd_binary_path: Optional[str] = None # None = use bundled binary + + +@dataclass +class EndpointPreferences: + """Preferences for network endpoint configuration.""" + + fulcrum_nodes: List[str] = field(default_factory=lambda: [ + # Well-known public Fulcrum / Electrum BCH servers (same pre-load as Electron Cash) + "wss://bch.imaginary.cash:50004", + "wss://electroncash.de:50004", + "wss://bch.loping.net:50004", + "wss://blackie.c3-soft.com:50004", + "wss://electroncash.dk:50002", + "wss://electron.jochen-hoenicke.de:51002", + "wss://bitcoincash.network:50004", + "wss://bch.soul-dev.com:50002", + ]) + nostr_relays: List[str] = field(default_factory=lambda: [ + # Well-known public Nostr relays (used as Phase 3 transport fallback) + "wss://relay.damus.io", + "wss://nos.lol", + "wss://relay.nostr.band", + "wss://relay.snort.social", + ]) + + @dataclass class OnboardingPreferences: """Track tutorial completion per tab.""" @@ -166,6 +202,8 @@ class UserPreferences: decision: DecisionConfig onboarding: OnboardingPreferences qwen3: Qwen3Preferences + relay: RelayPreferences + endpoints: EndpointPreferences def __init__(self): self.blocks = BlockPreferences() @@ -174,6 +212,8 @@ def __init__(self): self.decision = DecisionConfig() self.onboarding = OnboardingPreferences() self.qwen3 = Qwen3Preferences() + self.relay = RelayPreferences() + self.endpoints = EndpointPreferences() def to_dict(self) -> Dict[str, Any]: """Convert preferences to dictionary.""" @@ -190,6 +230,8 @@ def to_dict(self) -> Dict[str, Any]: 'decision': asdict(self.decision), 'onboarding': asdict(self.onboarding), 'qwen3': qwen3_dict, + 'relay': asdict(self.relay), + 'endpoints': asdict(self.endpoints), } @classmethod @@ -220,6 +262,24 @@ def from_dict(cls, data: Dict[str, Any]) -> 'UserPreferences': voice_library=qwen3_data.get('voice_library', {}), ) + if 'relay' in data: + relay_data = data['relay'] + prefs.relay = RelayPreferences( + relay_enabled=relay_data.get('relay_enabled', True), + relay_listen_port=relay_data.get('relay_listen_port', 0), + relay_max_connections=relay_data.get('relay_max_connections', 50), + relay_retention_days=relay_data.get('relay_retention_days', 7), + relay_custom_peers=relay_data.get('relay_custom_peers', []), + p2pd_binary_path=relay_data.get('p2pd_binary_path', None), + ) + + if 'endpoints' in data: + ep = data['endpoints'] + prefs.endpoints = EndpointPreferences( + fulcrum_nodes=ep.get('fulcrum_nodes', EndpointPreferences().fulcrum_nodes), + nostr_relays=ep.get('nostr_relays', EndpointPreferences().nostr_relays), + ) + return prefs @@ -686,6 +746,47 @@ def update_qwen3_preferences( self.save_preferences(prefs) return prefs + # ========================================================================= + # RELAY PREFERENCES + # ========================================================================= + + def get_relay_preferences(self) -> RelayPreferences: + """Get relay node preferences.""" + return self.load_preferences().relay + + def update_relay_preferences(self, **kwargs) -> UserPreferences: + """Update relay node preferences.""" + prefs = self.load_preferences() + for key, value in kwargs.items(): + if hasattr(prefs.relay, key): + setattr(prefs.relay, key, value) + self.save_preferences(prefs) + return prefs + + # ========================================================================= + # ENDPOINT PREFERENCES + # ========================================================================= + + def get_endpoint_preferences(self) -> EndpointPreferences: + """Get network endpoint preferences.""" + return self.load_preferences().endpoints + + def update_endpoint_preferences(self, **kwargs) -> UserPreferences: + """Update network endpoint preferences.""" + prefs = self.load_preferences() + for key, value in kwargs.items(): + if hasattr(prefs.endpoints, key): + setattr(prefs.endpoints, key, value) + self.save_preferences(prefs) + return prefs + + def reset_endpoint_preferences(self) -> UserPreferences: + """Reset endpoint preferences to defaults.""" + prefs = self.load_preferences() + prefs.endpoints = EndpointPreferences() + self.save_preferences(prefs) + return prefs + def get_voice_clones_dir(self) -> Path: """ Get the directory for storing voice clone audio files. diff --git a/gui_bridge.py b/gui_bridge.py index 768477b..284e817 100644 --- a/gui_bridge.py +++ b/gui_bridge.py @@ -63,21 +63,57 @@ def _quiet_excepthook(exc_type, exc_value, exc_tb): from dotenv import load_dotenv load_dotenv(Path(__file__).parent / ".env") +# ============================================================================ +# CUSTOM MODELS PATH (qubes-config.json or QUBES_MODELS_DIR env var) +# ============================================================================ +# Allow users to store models on any drive/path they want. +# Priority: QUBES_MODELS_DIR env var > qubes-config.json models_path > auto-detect +_custom_models_dir = None + +# 1. Check env var (set by .env file or OS) +if os.environ.get('QUBES_MODELS_DIR'): + _custom_models_dir = Path(os.environ['QUBES_MODELS_DIR']) + +# 2. Check qubes-config.json next to the exe +if _custom_models_dir is None: + try: + import json as _json + _exe_dir_cfg = Path(sys.executable).parent + for _cfg_path in [ + _exe_dir_cfg.parent / "qubes-config.json", + _exe_dir_cfg / "qubes-config.json", + Path(__file__).parent.parent / "qubes-config.json", + ]: + if _cfg_path.exists(): + _cfg = _json.loads(_cfg_path.read_text(encoding="utf-8")) + if _cfg.get("models_path"): + _custom_models_dir = Path(_cfg["models_path"]) + break + except Exception: + pass + # Auto-detect bundled HuggingFace models (heavy bundle) # Sets HF_HOME so kokoro, sentence-transformers find pre-downloaded models. -# Always set HF_HOME in bundled mode — if models aren't pre-bundled, -# HuggingFace will auto-download them to this location on first use. -if not os.environ.get('HF_HOME') and getattr(sys, 'frozen', False): - _exe_dir = Path(sys.executable).parent - # --onedir layout: exe is at Qubes/qubes-backend/qubes-backend - # models are at Qubes/models/huggingface (one level up) - _hf_models = _exe_dir.parent / "models" / "huggingface" - if not _hf_models.exists(): - # Flat layout or fresh install: use models next to exe - _hf_models = _exe_dir / "models" / "huggingface" - # Always set HF_HOME — create dir if needed, models will auto-download - _hf_models.mkdir(parents=True, exist_ok=True) - os.environ['HF_HOME'] = str(_hf_models) +# Priority: custom QUBES_MODELS_DIR config > bundled auto-detect. +# Always creates HF_HOME dir so models auto-download on first use. +if not os.environ.get('HF_HOME'): + if _custom_models_dir is not None: + _hf_models = _custom_models_dir / "huggingface" + _hf_models.mkdir(parents=True, exist_ok=True) + os.environ['HF_HOME'] = str(_hf_models) + os.environ.setdefault('QUBES_MODELS_DIR', str(_custom_models_dir)) + elif getattr(sys, 'frozen', False): + _exe_dir = Path(sys.executable).parent + # --onedir layout: exe is at Qubes/qubes-backend/qubes-backend + # models are at Qubes/models/huggingface (one level up) + _hf_models = _exe_dir.parent / "models" / "huggingface" + if not _hf_models.exists(): + # Flat layout or fresh install: use models next to exe + _hf_models = _exe_dir / "models" / "huggingface" + # Always set HF_HOME — create dir if needed, models will auto-download + _hf_models.mkdir(parents=True, exist_ok=True) + os.environ['HF_HOME'] = str(_hf_models) + os.environ.setdefault('QUBES_MODELS_DIR', str(_hf_models.parent)) # CRITICAL: Disable all logging to stdout/stderr before importing anything # Set environment variable to disable structlog output @@ -5006,6 +5042,249 @@ async def generate_introduction_message( logger.error(f"Failed to generate introduction: {e}", exc_info=True) return {"success": False, "error": str(e)} + # ========================================================================= + # RELAY NODE COMMANDS — Phase 1 + # ========================================================================= + + async def init_relay_node(self, user_id: str, password: str) -> Dict[str, Any]: + """Start the local P2P relay node.""" + try: + from network.relay_node import RelayNodeManager + from network.bundle_manager import get_p2pd_path + from pathlib import Path + + prefs = self.orchestrator.preferences_manager.get_relay_preferences() + if not prefs.relay_enabled: + return {"success": False, "error": "Relay is disabled in preferences"} + + if not hasattr(self, '_relay_node') or self._relay_node is None: + p2pd = prefs.p2pd_binary_path or str(get_p2pd_path() or "") + self._relay_node = RelayNodeManager( + user_data_dir=self.orchestrator.data_dir, + listen_port=prefs.relay_listen_port, + max_connections=prefs.relay_max_connections, + retention_days=prefs.relay_retention_days, + p2pd_binary=p2pd or None, + custom_peers=prefs.relay_custom_peers, + ) + + await self._relay_node.start() + status = self._relay_node.get_status() + return {"success": True, **status} + except Exception as e: + logger.error(f"init_relay_node failed: {e}", exc_info=True) + return {"success": False, "error": str(e)} + + async def stop_relay_node(self, user_id: str) -> Dict[str, Any]: + """Stop the local P2P relay node.""" + try: + if hasattr(self, '_relay_node') and self._relay_node is not None: + await self._relay_node.stop() + self._relay_node = None + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def get_relay_status(self, user_id: str) -> Dict[str, Any]: + """Get current relay node status.""" + try: + if not hasattr(self, '_relay_node') or self._relay_node is None: + return {"success": True, "running": False, "peer_id": None, "multiaddr": None, "peer_count": 0, "online_peers": 0} + status = self._relay_node.get_status() + return {"success": True, **status} + except Exception as e: + return {"success": False, "error": str(e)} + + async def get_relay_peers(self, user_id: str) -> Dict[str, Any]: + """Get list of relay peers with live reachability status.""" + try: + if not hasattr(self, '_relay_node') or self._relay_node is None: + from network.relay_list import BUILTIN_RELAYS + peers = [] + for r in BUILTIN_RELAYS: + for addr in r["multiaddrs"]: + peers.append({"multiaddr": addr, "label": r["label"], "operator": r["operator"], "builtin": True, "online": None, "latency_ms": None}) + return {"success": True, "peers": peers} + peers = self._relay_node.get_peers() + return {"success": True, "peers": peers} + except Exception as e: + return {"success": False, "error": str(e)} + + async def add_relay_peer(self, user_id: str, multiaddr: str) -> Dict[str, Any]: + """Add a custom relay peer.""" + try: + prefs = self.orchestrator.preferences_manager.get_relay_preferences() + if multiaddr not in prefs.relay_custom_peers: + prefs.relay_custom_peers.append(multiaddr) + self.orchestrator.preferences_manager.update_relay_preferences( + relay_custom_peers=prefs.relay_custom_peers + ) + if hasattr(self, '_relay_node') and self._relay_node: + await self._relay_node.add_peer(multiaddr) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def remove_relay_peer(self, user_id: str, multiaddr: str) -> Dict[str, Any]: + """Remove a custom relay peer.""" + try: + prefs = self.orchestrator.preferences_manager.get_relay_preferences() + if multiaddr in prefs.relay_custom_peers: + prefs.relay_custom_peers.remove(multiaddr) + self.orchestrator.preferences_manager.update_relay_preferences( + relay_custom_peers=prefs.relay_custom_peers + ) + if hasattr(self, '_relay_node') and self._relay_node: + await self._relay_node.remove_peer(multiaddr) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def update_relay_preferences(self, user_id: str, password: str, **kwargs) -> Dict[str, Any]: + """Update relay node preferences.""" + try: + prefs = self.orchestrator.preferences_manager.update_relay_preferences(**kwargs) + from dataclasses import asdict + return {"success": True, "relay": asdict(prefs.relay)} + except Exception as e: + return {"success": False, "error": str(e)} + + async def send_direct_p2p_message( + self, + qube_id: str, + recipient_qube_id: str, + recipient_pub_key: str, + message: str, + password: str, + ) -> Dict[str, Any]: + """Send an encrypted direct P2P message to another Qube via relay.""" + try: + from network.messaging import QubeMessage, EncryptedSession + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.backends import default_backend + import json, base64 + + self.orchestrator.set_master_key(password) + qube = self.orchestrator.qubes.get(qube_id) + if not qube: + return {"success": False, "error": f"Qube {qube_id} not loaded"} + + # Encrypt message payload using ECIES-like pattern + payload = json.dumps({"type": "text", "content": message, "sender_qube_id": qube_id}).encode() + + if not hasattr(self, '_relay_node') or self._relay_node is None: + return {"success": False, "error": "Relay node not running. Start relay first."} + + await self._relay_node.send_message( + recipient_qube_id=recipient_qube_id, + encrypted_payload=payload, # TODO Phase 2: wrap with onion + ttl_days=7, + ) + return {"success": True} + except Exception as e: + logger.error(f"send_direct_p2p_message failed: {e}", exc_info=True) + return {"success": False, "error": str(e)} + + async def get_direct_p2p_messages(self, qube_id: str, password: str) -> Dict[str, Any]: + """Drain store-and-forward queue for a Qube.""" + try: + if not hasattr(self, '_relay_node') or self._relay_node is None: + return {"success": True, "messages": []} + messages = await self._relay_node.get_pending_messages(qube_id) + return {"success": True, "messages": messages} + except Exception as e: + return {"success": False, "error": str(e)} + + async def check_relay_bundle_update(self, user_id: str) -> Dict[str, Any]: + """Check if a newer relay bundle is available. Phase 5.""" + try: + from network.bundle_manager import check_for_update + from pathlib import Path + result = await check_for_update(self.orchestrator.data_dir) + return {"success": True, **result} + except Exception as e: + return {"success": False, "error": str(e)} + + async def update_relay_bundle(self, user_id: str) -> Dict[str, Any]: + """Download and apply the latest relay bundle. Phase 5.""" + try: + from network.bundle_manager import update_bundle + result = await update_bundle(self.orchestrator.data_dir) + return {"success": True, **result} + except Exception as e: + return {"success": False, "error": str(e)} + + # ========================================================================= + # ENDPOINT PREFERENCES COMMANDS + # ========================================================================= + + async def get_endpoint_preferences(self, user_id: str) -> Dict[str, Any]: + """Get network endpoint preferences.""" + try: + from dataclasses import asdict + prefs = self.orchestrator.preferences_manager.get_endpoint_preferences() + return {"success": True, "endpoints": asdict(prefs)} + except Exception as e: + return {"success": False, "error": str(e)} + + async def update_endpoint_preferences(self, user_id: str, password: str, **kwargs) -> Dict[str, Any]: + """Update network endpoint preferences.""" + try: + from dataclasses import asdict + prefs = self.orchestrator.preferences_manager.update_endpoint_preferences(**kwargs) + return {"success": True, "endpoints": asdict(prefs.endpoints)} + except Exception as e: + return {"success": False, "error": str(e)} + + async def reset_endpoint_preferences(self, user_id: str, password: str) -> Dict[str, Any]: + """Reset endpoint preferences to defaults.""" + try: + from dataclasses import asdict + prefs = self.orchestrator.preferences_manager.reset_endpoint_preferences() + return {"success": True, "endpoints": asdict(prefs.endpoints)} + except Exception as e: + return {"success": False, "error": str(e)} + + async def check_endpoints(self, user_id: str) -> Dict[str, Any]: + """Ping all configured Fulcrum and Nostr endpoints via real WebSocket handshake.""" + import asyncio + + async def _wss_reachable(url: str, timeout: float = 6.0) -> bool: + """Open a WebSocket connection and immediately close it — confirms WSS is up.""" + try: + import websockets + async with asyncio.timeout(timeout): + async with websockets.connect(url, open_timeout=timeout, close_timeout=2) as ws: + await ws.close() + return True + except Exception: + return False + + try: + prefs = self.orchestrator.preferences_manager.get_endpoint_preferences() + fulcrum = prefs.fulcrum_nodes or [] + nostr = prefs.nostr_relays or [] + + fulcrum_results, nostr_results = await asyncio.gather( + asyncio.gather(*[_wss_reachable(u) for u in fulcrum]), + asyncio.gather(*[_wss_reachable(u) for u in nostr]), + ) + return { + "success": True, + "fulcrum": { + "connected": sum(fulcrum_results), + "total": len(fulcrum), + "status": list(fulcrum_results), + }, + "nostr": { + "connected": sum(nostr_results), + "total": len(nostr), + "status": list(nostr_results), + }, + } + except Exception as e: + return {"success": False, "error": str(e)} + async def send_introduction(self, qube_id: str, to_commitment: str, message: str, password: str) -> Dict[str, Any]: """Send an introduction request to another Qube""" try: @@ -6763,6 +7042,8 @@ async def export_account_backup_ipfs(self, export_password: str, master_password ipfs_cid = upload_result["IpfsHash"] logger.info(f"Account backup uploaded to IPFS: {ipfs_cid}") + # Unpin old versions — keep only the latest + await self._pinata_unpin_old(pinata_key, filename, keep_cid=ipfs_cid) return { "success": True, "ipfs_cid": ipfs_cid, @@ -6780,6 +7061,47 @@ async def export_account_backup_ipfs(self, export_password: str, master_password except Exception: pass + async def _pinata_unpin_old(self, pinata_key: str, filename: str, keep_cid: str) -> int: + """ + Unpin all Pinata pins whose metadata name equals `filename`, except `keep_cid`. + Called after a successful upload to keep only the latest backup per Qube/account. + Returns the number of CIDs unpinned. + """ + import aiohttp + unpinned = 0 + try: + headers = {"Authorization": f"Bearer {pinata_key}"} + params = {"metadata[name]": filename, "status": "pinned", "pageLimit": "100"} + async with aiohttp.ClientSession() as session: + async with session.get( + "https://api.pinata.cloud/data/pinList", + headers=headers, + params=params, + timeout=aiohttp.ClientTimeout(total=30) + ) as resp: + if resp.status != 200: + return 0 + data = await resp.json() + rows = data.get("rows", []) + + for row in rows: + cid = row.get("ipfs_pin_hash", "") + if cid and cid != keep_cid: + async with aiohttp.ClientSession() as s: + async with s.delete( + f"https://api.pinata.cloud/pinning/unpin/{cid}", + headers=headers, + timeout=aiohttp.ClientTimeout(total=15) + ) as del_resp: + if del_resp.status == 200: + unpinned += 1 + logger.info(f"Unpinned old backup {cid} ({filename})") + else: + logger.warning(f"Failed to unpin {cid}: {del_resp.status}") + except Exception as e: + logger.warning(f"_pinata_unpin_old failed for {filename}: {e}") + return unpinned + async def list_account_backups_pinata(self, pinata_jwt: str) -> Dict[str, Any]: """ List account backups stored in Pinata by querying the pin list API. @@ -7414,6 +7736,8 @@ def _read_dir_as_b64(root_dir: Path) -> dict: ipfs_cid = upload_result["IpfsHash"] logger.info(f"Qube {qube_id} backup uploaded to IPFS: {ipfs_cid}") + # Unpin old versions — keep only the latest per Qube + await self._pinata_unpin_old(pinata_key, filename, keep_cid=ipfs_cid) return { "success": True, "ipfs_cid": ipfs_cid, @@ -7431,7 +7755,7 @@ def _read_dir_as_b64(root_dir: Path) -> dict: except Exception: pass - async def import_account_backup_ipfs(self, ipfs_cid: str, import_password: str, master_password: str, wallet_sig: str = "") -> Dict[str, Any]: + async def import_account_backup_ipfs(self, ipfs_cid: str, import_password: str, master_password: str, wallet_sig: str = "", wallet_address: str = "") -> Dict[str, Any]: """ Restore full account backup from IPFS. @@ -7485,7 +7809,7 @@ async def import_account_backup_ipfs(self, ipfs_cid: str, import_password: str, tmp.write(backup_bytes) tmp_path = tmp.name - return await self.import_account_backup(tmp_path, import_password, master_password, wallet_sig=wallet_sig) + return await self.import_account_backup(tmp_path, import_password, master_password, wallet_sig=wallet_sig, wallet_address=wallet_address) except Exception as e: logger.error(f"Failed to import account backup from IPFS: {e}", exc_info=True) @@ -7538,7 +7862,7 @@ async def cleanup_incomplete_qubes(self) -> Dict[str, Any]: logger.error(f"Failed to cleanup incomplete qubes: {e}", exc_info=True) return {"success": False, "error": str(e)} - async def import_account_backup(self, import_path: str, import_password: str, master_password: str, wallet_sig: str = "") -> Dict[str, Any]: + async def import_account_backup(self, import_path: str, import_password: str, master_password: str, wallet_sig: str = "", wallet_address: str = "") -> Dict[str, Any]: """ Import a full account backup from a .qube-backup file. @@ -7653,6 +7977,14 @@ async def import_account_backup(self, import_path: str, import_password: str, ma imported_count = 0 skipped_count = 0 + # Build NFT verifier once if wallet_address provided — used to + # reject Qubes the wallet no longer owns (e.g. transferred away + # before this backup was made). + _nft_verifier = None + if wallet_address: + from blockchain.chain_sync import ChainSyncService + _nft_verifier = ChainSyncService().nft_verifier + for qube_entry in manifest.get("qube_index", []): qube_file = qube_entry["file"] qube_name_entry = qube_entry["name"] @@ -7677,6 +8009,25 @@ async def import_account_backup(self, import_path: str, import_password: str, ma except Exception: return {"success": False, "error": f"Wrong password — could not decrypt Qube {qube_name_entry}."} + # ── NFT Ownership Check ─────────────────────────────────────────────── + # Reject Qubes whose NFT the restoring wallet no longer owns. + # This closes the "old backup" loophole: if you transferred Van's NFT + # away, an old backup file still containing Van should not restore it. + # Qubes without nft_metadata (never minted) are restored unconditionally. + if _nft_verifier: + _nft_meta = qube_export.get("nft_metadata") or {} + _category_id = _nft_meta.get("category_id") + if _category_id: + _owns = await _nft_verifier.verify_ownership(_category_id, wallet_address) + if not _owns: + logger.warning( + f"Skipping Qube '{qube_name_entry}': " + f"wallet {wallet_address[:16]}... does not own NFT {_category_id[:16]}..." + ) + skipped_count += 1 + continue + # ───────────────────────────────────────────────────────────────────── + qube_id = qube_export["qube_id"] qube_name = qube_export["qube_name"] qube_dir_name = f"{qube_name}_{qube_id}" @@ -12024,6 +12375,25 @@ def debug_log(msg): lock_file.unlink() debug_log(f"Lock file removed") + # IPFS sync after anchor — honour the auto_sync_ipfs_on_anchor setting + if blocks_anchored > 0: + try: + prefs = user_bridge.orchestrator.get_block_preferences() + if prefs.auto_sync_ipfs_on_anchor: + debug_log(f"auto_sync_ipfs_on_anchor enabled — syncing Qube to IPFS...") + sync_result = await user_bridge.sync_qube_to_ipfs_backup( + qube_id, password, password + ) + if sync_result.get("success"): + debug_log(f"IPFS sync OK, CID={sync_result.get('ipfs_cid')}") + logger.info("auto_anchor_ipfs_sync_completed", qube_id=qube_id, cid=sync_result.get("ipfs_cid")) + else: + debug_log(f"IPFS sync failed (non-fatal): {sync_result.get('error')}") + logger.warning("auto_anchor_ipfs_sync_failed", qube_id=qube_id, error=sync_result.get("error")) + except Exception as ipfs_err: + debug_log(f"IPFS sync error (non-fatal): {ipfs_err}") + logger.warning("auto_anchor_ipfs_sync_error", qube_id=qube_id, error=str(ipfs_err)) + except Exception as e: import traceback tb = traceback.format_exc() @@ -12583,6 +12953,87 @@ def debug_log(msg): result = await user_bridge.uninstall_gpu_acceleration(user_id) print(json.dumps(result)) + elif command == "sweep-qube-wallet": + if len(sys.argv) < 5: + print(json.dumps({"error": "user_id, qube_id, sweep_address required"}), file=sys.stderr) + sys.exit(1) + user_id = sys.argv[2] + qube_id = sys.argv[3] + sweep_address = sys.argv[4] + password = sys.argv[5] if len(sys.argv) > 5 else None + user_bridge = GUIBridge(user_id=user_id) + if password: + user_bridge.orchestrator.set_master_key(password) + swept_sats = await user_bridge.orchestrator._sweep_before_delete(qube_id, password, sweep_address) + print(json.dumps({"success": True, "swept_sats": swept_sats or 0})) + + elif command == "check-local-tts-models": + if len(sys.argv) < 3: + print(json.dumps({"error": "User ID required"}), file=sys.stderr) + sys.exit(1) + + hf_home = os.environ.get("HF_HOME", "") + models_dir = os.environ.get("QUBES_MODELS_DIR", "") + + kokoro_installed = False + st_installed = False + whisper_installed = False + + if hf_home: + hf_path = Path(hf_home) + kokoro_path = hf_path / "hub" / "models--hexgrad--Kokoro-82M" + st_path = hf_path / "hub" / "models--sentence-transformers--all-MiniLM-L6-v2" + kokoro_installed = kokoro_path.exists() + st_installed = st_path.exists() + + if models_dir: + whisper_path = Path(models_dir) / "whisper" + whisper_installed = any(whisper_path.glob("*.bin")) if whisper_path.exists() else False + + print(json.dumps({ + "kokoro_installed": kokoro_installed, + "sentence_transformers_installed": st_installed, + "whisper_installed": whisper_installed, + "hf_home": hf_home, + "models_dir": str(models_dir or hf_home), + })) + + elif command == "update-local-tts-models": + if len(sys.argv) < 3: + print(json.dumps({"error": "User ID required"}), file=sys.stderr) + sys.exit(1) + + updated = [] + errors = [] + + try: + from huggingface_hub import snapshot_download + hf_home = os.environ.get("HF_HOME", None) + dl_kwargs = {} + if hf_home: + dl_kwargs["cache_dir"] = str(Path(hf_home) / "hub") + + try: + snapshot_download("hexgrad/Kokoro-82M", local_files_only=False, **dl_kwargs) + updated.append("kokoro-82m") + except Exception as e: + errors.append(f"Kokoro: {str(e)}") + + try: + snapshot_download("sentence-transformers/all-MiniLM-L6-v2", local_files_only=False, **dl_kwargs) + updated.append("sentence-transformers") + except Exception as e: + errors.append(f"sentence-transformers: {str(e)}") + + except ImportError as e: + errors.append(f"huggingface_hub not available: {str(e)}") + + print(json.dumps({ + "success": len(errors) == 0, + "updated": updated, + "errors": errors, + })) + elif command == "update-qwen3-preferences": if len(sys.argv) < 3: print(json.dumps({"error": "User ID required"}), file=sys.stderr) @@ -14766,6 +15217,107 @@ def debug_log(msg): ) print(json.dumps(result)) + # ===================================================================== + # RELAY NODE COMMANDS — Phase 1 + # ===================================================================== + elif command == "init-relay-node": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + password = get_secret("password", argv_index=3) + result = await user_bridge.init_relay_node(user_id, password) + print(json.dumps(result)) + + elif command == "stop-relay-node": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + result = await user_bridge.stop_relay_node(user_id) + print(json.dumps(result)) + + elif command == "get-relay-status": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + result = await user_bridge.get_relay_status(user_id) + print(json.dumps(result)) + + elif command == "get-relay-peers": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + result = await user_bridge.get_relay_peers(user_id) + print(json.dumps(result)) + + elif command == "add-relay-peer": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + multiaddr = sys.argv[3] if len(sys.argv) > 3 else "" + result = await user_bridge.add_relay_peer(user_id, multiaddr) + print(json.dumps(result)) + + elif command == "remove-relay-peer": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + multiaddr = sys.argv[3] if len(sys.argv) > 3 else "" + result = await user_bridge.remove_relay_peer(user_id, multiaddr) + print(json.dumps(result)) + + elif command == "update-relay-preferences": + import json as _json + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + password = get_secret("password", argv_index=3) + kwargs = _json.loads(sys.argv[4]) if len(sys.argv) > 4 else {} + result = await user_bridge.update_relay_preferences(user_id, password, **kwargs) + print(_json.dumps(result)) + + elif command == "send-direct-p2p-message": + if len(sys.argv) < 6: + print(json.dumps({"error": "user_id, qube_id, recipient_qube_id, recipient_pub_key required"}), file=sys.stderr) + sys.exit(1) + user_id = sys.argv[2] + qube_id = validate_qube_id(sys.argv[3]) + recipient_qube_id = sys.argv[4] + recipient_pub_key = sys.argv[5] + message = sys.argv[6] if len(sys.argv) > 6 else "" + password = get_secret("password", argv_index=7) + result = await user_bridge.send_direct_p2p_message(qube_id, recipient_qube_id, recipient_pub_key, message, password) + print(json.dumps(result)) + + elif command == "get-direct-p2p-messages": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + qube_id = validate_qube_id(sys.argv[3]) if len(sys.argv) > 3 else "" + password = get_secret("password", argv_index=4) + result = await user_bridge.get_direct_p2p_messages(qube_id, password) + print(json.dumps(result)) + + elif command == "check-relay-bundle-update": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + result = await user_bridge.check_relay_bundle_update(user_id) + print(json.dumps(result)) + + elif command == "update-relay-bundle": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + result = await user_bridge.update_relay_bundle(user_id) + print(json.dumps(result)) + + # ===================================================================== + # ENDPOINT PREFERENCES COMMANDS + # ===================================================================== + elif command == "get-endpoint-preferences": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + result = await user_bridge.get_endpoint_preferences(user_id) + print(json.dumps(result)) + + elif command == "update-endpoint-preferences": + import json as _json + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + password = get_secret("password", argv_index=3) + kwargs = _json.loads(sys.argv[4]) if len(sys.argv) > 4 else {} + result = await user_bridge.update_endpoint_preferences(user_id, password, **kwargs) + print(_json.dumps(result)) + + elif command == "reset-endpoint-preferences": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + password = get_secret("password", argv_index=3) + result = await user_bridge.reset_endpoint_preferences(user_id, password) + print(json.dumps(result)) + + elif command == "check-endpoints": + user_id = sys.argv[2] if len(sys.argv) > 2 else "default_user" + result = await user_bridge.check_endpoints(user_id) + print(json.dumps(result)) + elif command == "send-introduction": if len(sys.argv) < 6: print(json.dumps({"error": "User ID, Qube ID, to_commitment, and message required"}), file=sys.stderr) diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000..47cc2d2 --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,74 @@ +# Qubes Mobile Relay — Phase 4 + +Platform-specific relay implementations for mobile devices. + +--- + +## Android + +**Full relay node** via gomobile bindings. + +### Architecture + +``` +Android App (Kotlin/Java) + ↓ JNI +relay.aar (built with gomobile from go-libp2p) + ↓ +go-libp2p full node (same as desktop) + ↓ +Android Nearby API (BLE + WiFi-Direct) +``` + +### Build + +```bash +# Requires: Go 1.21+, gomobile, Android NDK +cd go/android-relay/ +gomobile init +gomobile bind -target=android -o ../../qubes-android/app/libs/relay.aar ./... +``` + +### Background mode + +Uses Android `WorkManager` with `KEEP_ALIVE` constraints. Cover traffic rate +is reduced to 0.1 Hz on battery to preserve battery life. + +--- + +## iOS + +**Constrained client** (no background relay daemon due to Apple restrictions). + +### Architecture + +``` +iOS App (Swift) + ↓ foreground +WebSocket connection to preferred relay (live messaging) + ↓ background +APNS notification wake → reconnect → drain store-and-forward queue +``` + +### APNS Setup + +1. Generate APNS authentication key at developer.apple.com +2. Place `.p8` key file on BitFaced relay servers +3. Configure `APNSBridge(apns_key_path=..., team_id=..., key_id=...)` + +### Privacy + +The APNS notification payload is empty. Apple never sees message content +or sender identity. The device token is rotated on each app launch. + +--- + +## Transport priority on mobile + +| Priority | Transport | Android | iOS | +|----------|-----------|---------|-----| +| 1 | Local relay daemon | ✅ Full | ❌ Not available | +| 2 | BLE mesh (Nearby) | ✅ Full | ✅ Multipeer | +| 3 | Internet DHT (libp2p) | ✅ Full | ✅ Client only | +| 4 | Nostr fallback | ✅ | ✅ | +| 5 | APNS wake + reconnect | — | ✅ | diff --git a/mobile/android/relay_node_android.py b/mobile/android/relay_node_android.py new file mode 100644 index 0000000..608382c --- /dev/null +++ b/mobile/android/relay_node_android.py @@ -0,0 +1,53 @@ +""" +Android Relay Node — Phase 4 (Scaffold) + +Full relay node for Android via gomobile bindings. + +Architecture: + - Go library built with gomobile: go-libp2p full node + - Python sidecar calls into the Go library via JNI bridge + - Android Nearby API for BLE + WiFi-Direct discovery + - Background service using WorkManager for keepalive + +gomobile build: + cd go/android-relay + gomobile bind -target=android -o relay.aar ./... + +The .aar is bundled in the Android APK and exposed via JNI to the Python sidecar. + +Key binding surface (mirrors RelayNodeManager interface): + + func Start(port int, maxConns int) string # returns peer_id + func Stop() + func GetStatus() string # JSON + func AddPeer(multiaddr string) bool + func RemovePeer(multiaddr string) bool + func SendMessage(recipientId string, payload []byte, ttlDays int) bool + func GetPendingMessages(qubeId string) string # JSON array + +TODO Phase 4: + 1. Create go/android-relay/ Go module with gomobile bindings + 2. Add JNI bridge in qubes-android/app/src/main/jni/ + 3. Call Start() from Android background service + 4. Wire Android Nearby API to ble_transport + 5. Implement battery-aware background mode (reduce cover traffic rate on battery) +""" + +from utils.logging import get_logger + +logger = get_logger(__name__) + + +class AndroidRelayNode: + """ + Stub for the Android gomobile relay node binding. + + On Android, this class is replaced by a JNI call into the compiled .aar. + On desktop, the standard RelayNodeManager is used instead. + """ + + def __init__(self): + raise NotImplementedError( + "AndroidRelayNode requires the gomobile .aar build. " + "Use RelayNodeManager on desktop." + ) diff --git a/mobile/ios/apns_bridge.py b/mobile/ios/apns_bridge.py new file mode 100644 index 0000000..4a26cb4 --- /dev/null +++ b/mobile/ios/apns_bridge.py @@ -0,0 +1,83 @@ +""" +iOS APNS Notification Bridge — Phase 4 (Scaffold) + +iOS kills background processes aggressively. The iOS Qubes app cannot run +a full relay daemon. Instead: + + 1. In foreground: maintains a live WebSocket connection to the user's + preferred relay (Priority-1 transport as normal). + + 2. In background: registers a lightweight APNS notification token. + When a relay receives a message for this user, it sends a push + notification via APNS: {"aps": {"alert": "", "badge": 1, "sound": "default"}}. + The actual message content never touches Apple's servers. + + 3. On APNS wake: app reconnects to relay, downloads pending messages + from the store-and-forward queue, delivers to the Qube memory chain. + +Server-side APNS sender (runs on BitFaced seed relays): + - Python aiohttp server receiving "notify_user" events from relay + - Calls APNS HTTP/2 API with the user's device token + - Device token stored in relay DHT: sha256(qube_id) → {"apns_token": "...", "platform": "ios"} + +Privacy note: + - The APNS notification payload is empty (no message content, no sender ID). + - Only the BitFaced relay knows which device token to ping — not Apple. + - Device token is rotated on each app launch for additional privacy. + +TODO Phase 4: + 1. iOS Swift: register for APNS notifications, send token to relay via DHT + 2. Python relay server: implement notify_user DHT subscription handler + 3. APNS sender: POST to api.push.apple.com/3/device/{token} + 4. iOS app: handle background fetch on APNS wake, reconnect to relay +""" + +from typing import Optional +from utils.logging import get_logger + +logger = get_logger(__name__) + + +class APNSBridge: + """ + Stub for the iOS APNS notification bridge. + + Server-side component (runs on relay nodes to wake iOS devices). + """ + + def __init__(self, apns_key_path: Optional[str] = None, team_id: str = "", key_id: str = ""): + """ + Args: + apns_key_path: Path to Apple .p8 authentication key file. + team_id: Apple Developer Team ID. + key_id: APNS authentication key ID. + """ + self.apns_key_path = apns_key_path + self.team_id = team_id + self.key_id = key_id + + async def notify(self, device_token: str, badge: int = 1) -> bool: + """ + Send a silent push notification to wake an iOS device. + + The notification payload is intentionally empty — it just wakes the app. + The app then reconnects and fetches queued messages directly from the relay. + + TODO Phase 4: implement APNS HTTP/2 API call. + """ + logger.info("apns_notify_stub", token=device_token[:8] + "...", badge=badge) + return False # Phase 4 TODO + + async def register_device_token( + self, qube_id: str, device_token: str, relay_dht_bridge + ) -> bool: + """ + Publish device token to DHT so relay nodes can wake this device. + + Key: sha256(qube_id) + Value: {"apns_token": device_token, "platform": "ios", "expires": timestamp} + + TODO Phase 4: implement DHT put via relay_dht_bridge. + """ + logger.info("apns_register_stub", qube_id=qube_id[:8], token=device_token[:8] + "...") + return False # Phase 4 TODO diff --git a/network/ble_transport.py b/network/ble_transport.py new file mode 100644 index 0000000..9e23d56 --- /dev/null +++ b/network/ble_transport.py @@ -0,0 +1,347 @@ +""" +BLE Transport Driver — Phase 6 + +Bluetooth Low Energy transport for offline mesh communication between +nearby Qubes devices (no internet required). + +Implements the 4-function relay transport interface: + connect(peer_multiaddr) → stream + send(stream, bytes) → None + receive(stream) → bytes + discover_local_peers() → List[str] + +Also implements BLE GATT server (peripheral/advertise role) via bless: + BLEServer — advertises the Qubes relay service so other devices can + discover and connect to this node without internet (BitChat-parity). + +Requires: + pip install bleak>=0.21.0 # Central (client/scan) role + pip install bless>=0.2.5 # Peripheral (server/advertise) role + +If bleak/bless are not installed, the respective class raises ImportError +on instantiation and the relay falls back gracefully. + +BLE GATT service: + Service UUID: QUBES_BLE_SERVICE_UUID + Write char UUID: QUBES_BLE_WRITE_CHAR (client → server) + Notify char UUID: QUBES_BLE_NOTIFY_CHAR (server → client) + +Message framing: + Messages are fragmented into CHUNK_SIZE (488) byte pieces. + Each fragment: [seq_4b][total_4b][data] + Final reassembly at receiver. + +Platform notes: + Windows: WinRT BLE stack (bleak) + WinRT GattServiceProvider (bless) + macOS: CoreBluetooth via bleak + bless + Linux: BlueZ via D-Bus (requires bluetoothd running) + Android: Handled by the mobile layer (not this file) + iOS: CoreBluetooth (handled by mobile layer) +""" + +import asyncio +import struct +from typing import Any, Callable, Dict, List, Optional + +from utils.logging import get_logger + +logger = get_logger(__name__) + +# Fixed UUIDs for Qubes relay BLE service +QUBES_BLE_SERVICE_UUID = "12a24d53-dcc0-75dd-7226-ea319f20d43d" +QUBES_BLE_WRITE_CHAR = "12a24d54-dcc0-75dd-7226-ea319f20d43d" +QUBES_BLE_NOTIFY_CHAR = "12a24d55-dcc0-75dd-7226-ea319f20d43d" + +# BLE MTU constraint — fragment size in bytes (leave room for framing) +CHUNK_SIZE = 488 # 512 - 24 byte framing overhead + +# Discovery scan duration +SCAN_TIMEOUT_SECS = 5.0 + +try: + from bleak import BleakScanner, BleakClient + _BLEAK_AVAILABLE = True +except ImportError: + _BLEAK_AVAILABLE = False + +try: + from bless import BlessServer, BlessGATTCharacteristic, GATTCharacteristicProperties, GATTAttributePermissions + _BLESS_AVAILABLE = True +except ImportError: + _BLESS_AVAILABLE = False + + +class BLETransport: + """ + BLE transport driver for Qubes P2P relay. + + Usage: + transport = BLETransport() + peers = await transport.discover_local_peers() + stream = await transport.connect(peers[0]) + await transport.send(stream, b"hello") + data = await transport.receive(stream) + await stream.disconnect() + + The stream object is a BleakClient instance. + """ + + def __init__(self) -> None: + if not _BLEAK_AVAILABLE: + raise ImportError( + "bleak is required for BLE transport. Install with: pip install bleak>=0.21.0" + ) + self._receive_buffers: Dict[str, bytearray] = {} + self._receive_events: Dict[str, asyncio.Event] = {} + + async def discover_local_peers(self) -> List[str]: + """ + Scan for nearby BLE devices advertising the Qubes relay service. + + Returns: + List of multiaddr strings: ["/ble/MAC:AA:BB:CC:DD:EE:FF/p2p/...", ...] + """ + logger.info("ble_scan_start", timeout=SCAN_TIMEOUT_SECS) + try: + devices = await BleakScanner.discover( + timeout=SCAN_TIMEOUT_SECS, + service_uuids=[QUBES_BLE_SERVICE_UUID], + ) + peers = [f"/ble/MAC:{d.address}/p2p/{d.name or 'unknown'}" for d in devices] + logger.info("ble_scan_done", peers_found=len(peers)) + return peers + except Exception as exc: + logger.warning("ble_scan_failed", error=str(exc)) + return [] + + async def connect(self, peer_multiaddr: str) -> Any: + """ + Connect to a BLE peer. + + Args: + peer_multiaddr: "/ble/MAC:AA:BB:CC:DD:EE:FF/p2p/..." format. + + Returns: + Connected BleakClient (the "stream" object). + """ + # Parse MAC address from multiaddr + parts = peer_multiaddr.split("/") + mac = "" + for part in parts: + if part.startswith("MAC:"): + mac = part[4:] # Strip "MAC:" prefix + break + + if not mac: + raise ValueError(f"Cannot parse BLE MAC from multiaddr: {peer_multiaddr}") + + client = BleakClient(mac) + await client.start_notify(QUBES_BLE_NOTIFY_CHAR, self._notification_handler) + await client.connect() + + # Initialize receive buffer for this connection + self._receive_buffers[mac] = bytearray() + self._receive_events[mac] = asyncio.Event() + + logger.info("ble_connected", mac=mac) + return client + + async def send(self, stream: Any, data: bytes) -> None: + """ + Send data over a connected BLE stream. + Fragments data into CHUNK_SIZE pieces with sequence framing. + + Args: + stream: Connected BleakClient from connect(). + data: Raw bytes to send. + """ + fragments = [data[i:i + CHUNK_SIZE] for i in range(0, len(data), CHUNK_SIZE)] + total = len(fragments) + + for seq, fragment in enumerate(fragments): + # Header: [seq uint32][total uint32][data] + header = struct.pack(">II", seq, total) + packet = header + fragment + await stream.write_gatt_char(QUBES_BLE_WRITE_CHAR, packet, response=True) + + mac = stream.address + logger.debug("ble_sent", mac=mac[:11], bytes=len(data), fragments=total) + + async def receive(self, stream: Any) -> bytes: + """ + Wait for and return the next complete message from a BLE stream. + Reassembles fragmented packets. + + Args: + stream: Connected BleakClient from connect(). + + Returns: + Reassembled message bytes. + """ + mac = stream.address + event = self._receive_events.get(mac) + if event: + await event.wait() + event.clear() + + data = bytes(self._receive_buffers.get(mac, bytearray())) + self._receive_buffers[mac] = bytearray() + return data + + def _notification_handler(self, sender: Any, data: bytes) -> None: + """GATT notification callback — accumulates fragments and signals completion.""" + try: + if len(data) < 8: + return + seq, total = struct.unpack_from(">II", data, 0) + payload = data[8:] + + # Use sender as key (may be characteristic UUID or address depending on platform) + key = str(sender) + + if key not in self._receive_buffers: + self._receive_buffers[key] = bytearray() + self._receive_events[key] = asyncio.Event() + + self._receive_buffers[key].extend(payload) + + # Signal complete when last fragment received + if seq == total - 1: + event = self._receive_events.get(key) + if event: + event.set() + + except Exception as exc: + logger.debug("ble_notification_error", error=str(exc)) + + +def is_ble_available() -> bool: + """Return True if bleak is installed and BLE transport can be used.""" + return _BLEAK_AVAILABLE + + +def is_ble_server_available() -> bool: + """Return True if bless is installed and BLE advertising is supported.""" + return _BLESS_AVAILABLE + + +class BLEServer: + """ + BLE GATT peripheral (server/advertise) role for Qubes relay. + + Advertises the Qubes relay service UUID so nearby devices running + BLETransport can discover this node and connect without internet. + This is the missing piece for BitChat-parity BLE mesh. + + Requires: pip install bless>=0.2.5 + + Usage: + server = BLEServer(peer_id="QmMyPeerID", on_message=handle_message) + await server.start() + # ... relay is now discoverable via BLE scan ... + await server.stop() + + on_message(sender_addr: str, data: bytes) is called for each complete + reassembled message received from a connecting BLE client. + """ + + def __init__( + self, + peer_id: str, + on_message: Optional[Callable[[str, bytes], None]] = None, + ) -> None: + if not _BLESS_AVAILABLE: + raise ImportError( + "bless is required for BLE server mode. Install with: pip install bless>=0.2.5" + ) + self.peer_id = peer_id + self.on_message = on_message + self._server: Optional[Any] = None + self._running = False + self._receive_buffers: Dict[str, bytearray] = {} + self._receive_seqs: Dict[str, int] = {} + + async def start(self) -> None: + """Start advertising the Qubes BLE relay service.""" + loop = asyncio.get_event_loop() + self._server = BlessServer(name=f"Qubes-{self.peer_id[:8]}", loop=loop) + self._server.read_request_func = self._read_request + self._server.write_request_func = self._write_request + + await self._server.add_new_service(QUBES_BLE_SERVICE_UUID) + + # Write characteristic: remote → us (receive incoming data) + write_props = GATTCharacteristicProperties.write | GATTCharacteristicProperties.write_without_response + write_perms = GATTAttributePermissions.writeable + await self._server.add_new_characteristic( + QUBES_BLE_SERVICE_UUID, + QUBES_BLE_WRITE_CHAR, + write_props, + None, + write_perms, + ) + + # Notify characteristic: us → remote (send outgoing data) + notify_props = GATTCharacteristicProperties.notify | GATTCharacteristicProperties.read + notify_perms = GATTAttributePermissions.readable + await self._server.add_new_characteristic( + QUBES_BLE_SERVICE_UUID, + QUBES_BLE_NOTIFY_CHAR, + notify_props, + None, + notify_perms, + ) + + await self._server.start() + self._running = True + logger.info("ble_server_started", peer_id=self.peer_id[:16], service=QUBES_BLE_SERVICE_UUID) + + async def stop(self) -> None: + """Stop BLE advertising and disconnect all clients.""" + if self._server and self._running: + await self._server.stop() + self._running = False + logger.info("ble_server_stopped") + + async def notify_all(self, data: bytes) -> None: + """Send data to all connected BLE clients via GATT notify.""" + if not self._server or not self._running: + return + fragments = [data[i:i + CHUNK_SIZE] for i in range(0, len(data), CHUNK_SIZE)] + total = len(fragments) + for seq, fragment in enumerate(fragments): + packet = struct.pack(">II", seq, total) + fragment + self._server.get_characteristic(QUBES_BLE_NOTIFY_CHAR).value = bytearray(packet) + self._server.update_value(QUBES_BLE_SERVICE_UUID, QUBES_BLE_NOTIFY_CHAR) + await asyncio.sleep(0.01) # yield between fragments + + def _read_request(self, characteristic: Any, **kwargs: Any) -> bytearray: + """GATT read handler — returns empty payload (notify-only design).""" + return bytearray() + + def _write_request(self, characteristic: Any, value: Any, **kwargs: Any) -> None: + """GATT write handler — reassemble fragments from BLE client.""" + try: + data = bytes(value) + if len(data) < 8: + return + seq, total = struct.unpack_from(">II", data, 0) + payload = data[8:] + + # Use characteristic handle as key (single-client simplification) + key = "default" + if key not in self._receive_buffers: + self._receive_buffers[key] = bytearray() + + self._receive_buffers[key].extend(payload) + + if seq == total - 1: + complete = bytes(self._receive_buffers.pop(key)) + logger.debug("ble_server_received", bytes=len(complete)) + if self.on_message: + try: + self.on_message("ble_client", complete) + except Exception as exc: + logger.debug("ble_server_message_handler_error", error=str(exc)) + except Exception as exc: + logger.debug("ble_server_write_error", error=str(exc)) diff --git a/network/bundle_manager.py b/network/bundle_manager.py new file mode 100644 index 0000000..506f985 --- /dev/null +++ b/network/bundle_manager.py @@ -0,0 +1,457 @@ +""" +Relay Bundle Manager — Phase 5 + +Manages the relay bundle: p2pd binary, relay list, and all networking +libraries as a single independently-updatable package stored inside the +Qubes app data directory. + +Bundle lives at: {qubes_data_dir}/relay_bundle/ (or D:\\Qubes\\relay\\ when installed) + relay/ + p2pd[.exe] — Go libp2p daemon binary + relay_list.json — Community relay list (updated separately from app) + bundle_version.txt — Current bundle version string + bundle_manifest.json — Version, checksums, update URL + +The Settings → Endpoints → "Update Bundle" button calls update_bundle(). +Signature verification is done against BitFaced's public key hardcoded here. +""" + +import asyncio +import hashlib +import json +import os +import platform +import shutil +import sys +import tarfile +import tempfile +import zipfile +from pathlib import Path +from typing import Any, Dict, Optional + +from utils.logging import get_logger + +logger = get_logger(__name__) + +# BitFaced's bundle signing public key (secp256k1 compressed hex). +# Hardcoded at compile time — bundles that fail this check are silently rejected. +BUNDLE_SIGNING_PUBKEY = "TODO_REPLACE_WITH_BITFACED_PUBKEY" + +# Canonical bundle manifest URL (mirrored by community volunteers). +BUNDLE_MANIFEST_URL = "https://qube.cash/relay-bundle/manifest.json" + +# Download timeout in seconds +DOWNLOAD_TIMEOUT = 120 + + +def _qubes_root_dir() -> Optional[Path]: + """ + Detect the Qubes install root — the folder that contains Qubes.exe / Qubes + alongside ollama/, qubes-backend/, etc. + + Search order: + 1. Parent of the running qubes-backend executable (frozen PyInstaller) + 2. None (caller falls back to project root) + """ + if getattr(sys, "frozen", False): + candidate = Path(sys.executable).parent.parent + if (candidate / "qubes-backend").is_dir(): + return candidate + return Path(sys.executable).parent + return None + + +def get_bundle_dir(qubes_data_dir: Optional[Path] = None) -> Path: + """ + Return the relay bundle directory. + + Resolution order (same pattern as D:\\Qubes\\ollama\\): + 1. Explicit qubes_data_dir argument + 2. D:\\Qubes\\relay\\ (or equivalent root/relay/) when running installed + 3. {project_root}/relay_bundle/ in dev mode + """ + if qubes_data_dir: + bundle_dir = Path(qubes_data_dir) / "relay_bundle" + else: + root = _qubes_root_dir() + if root: + bundle_dir = root / "relay" + else: + bundle_dir = Path(__file__).resolve().parent.parent / "relay_bundle" + + bundle_dir.mkdir(parents=True, exist_ok=True) + return bundle_dir + + +def get_p2pd_path(qubes_data_dir: Optional[Path] = None) -> Optional[Path]: + """ + Return path to the p2pd binary, or None if not present. + + Search order: + 1. relay/ folder (D:\\Qubes\\relay\\p2pd.exe) + 2. qubes-backend\\_internal\\ (bundled alongside Python sidecar) + 3. System PATH + """ + bundle_dir = get_bundle_dir(qubes_data_dir) + binary_name = "p2pd.exe" if platform.system() == "Windows" else "p2pd" + + bundled = bundle_dir / binary_name + if bundled.exists(): + return bundled + + if getattr(sys, "frozen", False): + internal = Path(sys.executable).parent / "_internal" / binary_name + if internal.exists(): + return internal + + system_p2pd = shutil.which("p2pd") + if system_p2pd: + return Path(system_p2pd) + + return None + + +def get_bundle_version(qubes_data_dir: Optional[Path] = None) -> str: + """Return the installed bundle version, or 'none' if not installed.""" + version_file = get_bundle_dir(qubes_data_dir) / "bundle_version.txt" + if version_file.exists(): + return version_file.read_text(encoding="utf-8").strip() + return "none" + + +def get_bundle_manifest(qubes_data_dir: Optional[Path] = None) -> Dict[str, Any]: + """Return the installed bundle manifest.""" + manifest_file = get_bundle_dir(qubes_data_dir) / "bundle_manifest.json" + if manifest_file.exists(): + try: + return json.loads(manifest_file.read_text(encoding="utf-8")) + except json.JSONDecodeError: + pass + return {} + + +# --------------------------------------------------------------------------- +# Signature verification +# --------------------------------------------------------------------------- + +def _verify_bundle_signature(file_sha256_hex: str, signature_hex: str) -> bool: + """ + Verify secp256k1 ECDSA signature of the bundle SHA-256 hash. + + Returns True if verification passes OR if BUNDLE_SIGNING_PUBKEY is the + placeholder (allows dev/testing without a real key). + """ + if "TODO" in BUNDLE_SIGNING_PUBKEY: + logger.warning("bundle_sig_skip_todo_pubkey") + return True # Dev mode: skip verification + + try: + from ecdsa import VerifyingKey, SECP256k1, BadSignatureError + + vk = VerifyingKey.from_string( + bytes.fromhex(BUNDLE_SIGNING_PUBKEY), curve=SECP256k1 + ) + sig_bytes = bytes.fromhex(signature_hex) + data_bytes = bytes.fromhex(file_sha256_hex) + vk.verify_digest(sig_bytes, data_bytes) + return True + except Exception as exc: + logger.warning("bundle_sig_invalid", error=str(exc)) + return False + + +def _sha256_file(path: Path) -> str: + """Return the SHA-256 hex digest of a file.""" + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + return h.hexdigest() + + +# --------------------------------------------------------------------------- +# Update logic +# --------------------------------------------------------------------------- + +async def _fetch_manifest() -> Optional[Dict[str, Any]]: + """Fetch the bundle manifest JSON from BUNDLE_MANIFEST_URL.""" + try: + import aiohttp + async with aiohttp.ClientSession() as session: + async with session.get( + BUNDLE_MANIFEST_URL, + timeout=aiohttp.ClientTimeout(total=15), + ) as resp: + resp.raise_for_status() + text = await resp.text() + return json.loads(text) + except Exception as exc: + logger.warning("bundle_manifest_fetch_failed", url=BUNDLE_MANIFEST_URL, error=str(exc)) + return None + + +async def _download_file(url: str, dest: Path) -> bool: + """Stream-download url to dest file. Returns True on success.""" + try: + import aiohttp + async with aiohttp.ClientSession() as session: + async with session.get( + url, + timeout=aiohttp.ClientTimeout(total=DOWNLOAD_TIMEOUT), + ) as resp: + resp.raise_for_status() + total = int(resp.headers.get("Content-Length", 0)) + downloaded = 0 + with open(dest, "wb") as f: + async for chunk in resp.content.iter_chunked(65536): + f.write(chunk) + downloaded += len(chunk) + if total: + pct = downloaded * 100 // total + logger.debug("bundle_download_progress", pct=pct) + logger.info("bundle_download_done", dest=str(dest), bytes=downloaded) + return True + except Exception as exc: + logger.warning("bundle_download_failed", url=url[:60], error=str(exc)) + return False + + +def _extract_bundle(archive_path: Path, dest_dir: Path) -> bool: + """Extract .zip or .tar.gz archive to dest_dir. Returns True on success.""" + try: + name = archive_path.name.lower() + if name.endswith(".zip"): + with zipfile.ZipFile(archive_path, "r") as z: + z.extractall(dest_dir) + elif name.endswith((".tar.gz", ".tgz")): + with tarfile.open(archive_path, "r:gz") as t: + t.extractall(dest_dir) + else: + logger.warning("bundle_unknown_format", name=archive_path.name) + return False + + # Make p2pd executable on non-Windows + if platform.system() != "Windows": + p2pd = dest_dir / "p2pd" + if p2pd.exists(): + p2pd.chmod(p2pd.stat().st_mode | 0o111) + + return True + except Exception as exc: + logger.warning("bundle_extract_failed", error=str(exc)) + return False + + +async def check_for_update(qubes_data_dir: Optional[Path] = None) -> Dict[str, Any]: + """ + Check if a newer relay bundle is available. + + Returns dict with: + current_version, latest_version, update_available, manifest (if fetched) + """ + current = get_bundle_version(qubes_data_dir) + manifest = await _fetch_manifest() + + if not manifest: + return { + "current_version": current, + "latest_version": "unknown", + "update_available": False, + "error": "Could not fetch manifest", + } + + latest = manifest.get("version", "unknown") + + def _version_tuple(v: str): + try: + return tuple(int(x) for x in v.split(".")) + except Exception: + return (0,) + + update_available = ( + latest != "unknown" + and current != "none" + and _version_tuple(latest) > _version_tuple(current) + ) or (current == "none" and latest != "unknown") + + return { + "current_version": current, + "latest_version": latest, + "update_available": update_available, + "manifest": manifest, + } + + +async def update_bundle(qubes_data_dir: Optional[Path] = None) -> Dict[str, Any]: + """ + Download, verify, and atomically apply the latest relay bundle. + + Steps: + 1. Fetch manifest from BUNDLE_MANIFEST_URL + 2. Compare version with installed + 3. Download bundle archive to temp file + 4. Verify SHA-256 checksum + 5. Verify secp256k1 signature against BUNDLE_SIGNING_PUBKEY + 6. Atomically replace relay/ directory + 7. Write bundle_version.txt and bundle_manifest.json + """ + bundle_dir = get_bundle_dir(qubes_data_dir) + + # Step 1-2: check if update is needed + check = await check_for_update(qubes_data_dir) + if not check.get("update_available"): + return { + "success": False, + "message": f"Already up to date (version {check.get('current_version')}).", + "current_version": check.get("current_version"), + } + + manifest = check["manifest"] + download_url = manifest.get("download_url") + expected_sha256 = manifest.get("sha256", "") + signature_hex = manifest.get("signature", "") + new_version = manifest.get("version", "unknown") + + if not download_url: + return {"success": False, "message": "Manifest missing download_url."} + + logger.info("bundle_update_start", version=new_version, url=download_url[:60]) + + # Step 3: download to temp file + with tempfile.TemporaryDirectory() as tmp_str: + tmp = Path(tmp_str) + archive_suffix = ".zip" if download_url.lower().endswith(".zip") else ".tar.gz" + archive_path = tmp / f"relay_bundle{archive_suffix}" + + if not await _download_file(download_url, archive_path): + return {"success": False, "message": "Download failed."} + + # Step 4: verify SHA-256 + if expected_sha256: + actual_sha256 = _sha256_file(archive_path) + if actual_sha256 != expected_sha256: + logger.warning( + "bundle_sha256_mismatch", + expected=expected_sha256, + actual=actual_sha256, + ) + return {"success": False, "message": "SHA-256 checksum mismatch — bundle corrupt."} + + # Step 5: verify signature + if signature_hex and expected_sha256: + if not _verify_bundle_signature(expected_sha256, signature_hex): + return {"success": False, "message": "Signature verification failed — bundle rejected."} + + # Step 6: extract to temp dir then atomically swap + extract_dir = tmp / "extracted" + extract_dir.mkdir() + if not _extract_bundle(archive_path, extract_dir): + return {"success": False, "message": "Archive extraction failed."} + + # Atomic replace: backup existing → move new in → delete backup + backup_dir = bundle_dir.parent / (bundle_dir.name + ".backup") + try: + if bundle_dir.exists(): + if backup_dir.exists(): + shutil.rmtree(backup_dir) + shutil.move(str(bundle_dir), str(backup_dir)) + + shutil.copytree(str(extract_dir), str(bundle_dir)) + + # Step 7: write metadata + (bundle_dir / "bundle_version.txt").write_text(new_version, encoding="utf-8") + (bundle_dir / "bundle_manifest.json").write_text( + json.dumps(manifest, indent=2), encoding="utf-8" + ) + + # Remove backup on success + if backup_dir.exists(): + shutil.rmtree(backup_dir) + + except Exception as exc: + # Restore backup if swap failed + if backup_dir.exists() and not bundle_dir.exists(): + shutil.move(str(backup_dir), str(bundle_dir)) + logger.warning("bundle_swap_failed", error=str(exc)) + return {"success": False, "message": f"Atomic replace failed: {exc}"} + + logger.info("bundle_update_done", version=new_version) + return { + "success": True, + "version": new_version, + "message": f"Bundle updated to {new_version}. Restart relay to apply.", + } + + +# --------------------------------------------------------------------------- +# p2pd auto-download on first run +# --------------------------------------------------------------------------- + +# go-libp2p-daemon release asset naming pattern +_P2PD_VERSION = "0.3.1" # Minimum supported version +_P2PD_BASE_URL = "https://github.com/libp2p/go-libp2p-daemon/releases/download" + +_P2PD_ASSETS: Dict[str, str] = { + "Windows-x86_64": f"{_P2PD_BASE_URL}/v{_P2PD_VERSION}/p2pd_windows_amd64.exe", + "Windows-AMD64": f"{_P2PD_BASE_URL}/v{_P2PD_VERSION}/p2pd_windows_amd64.exe", + "Darwin-x86_64": f"{_P2PD_BASE_URL}/v{_P2PD_VERSION}/p2pd_darwin_amd64", + "Darwin-arm64": f"{_P2PD_BASE_URL}/v{_P2PD_VERSION}/p2pd_darwin_arm64", + "Linux-x86_64": f"{_P2PD_BASE_URL}/v{_P2PD_VERSION}/p2pd_linux_amd64", + "Linux-aarch64": f"{_P2PD_BASE_URL}/v{_P2PD_VERSION}/p2pd_linux_arm64", +} + + +def _p2pd_download_url() -> Optional[str]: + """Return the download URL for p2pd matching the current OS/arch, or None.""" + key = f"{platform.system()}-{platform.machine()}" + return _P2PD_ASSETS.get(key) + + +async def ensure_p2pd_binary(qubes_data_dir: Optional[Path] = None) -> Optional[Path]: + """ + Ensure the p2pd binary is present. Downloads it automatically if missing. + + Called by RelayNodeManager.start() before spawning the daemon. + Returns the path to the binary, or None if download failed. + + The binary is placed in get_bundle_dir() / p2pd[.exe]. + """ + existing = get_p2pd_path(qubes_data_dir) + if existing: + logger.debug("p2pd_already_present", path=str(existing)) + return existing + + url = _p2pd_download_url() + if not url: + logger.warning( + "p2pd_no_download_url", + system=platform.system(), + machine=platform.machine(), + hint="Download p2pd manually from https://github.com/libp2p/go-libp2p-daemon/releases", + ) + return None + + bundle_dir = get_bundle_dir(qubes_data_dir) + binary_name = "p2pd.exe" if platform.system() == "Windows" else "p2pd" + dest = bundle_dir / binary_name + + logger.info("p2pd_auto_download_start", url=url, dest=str(dest)) + + try: + success = await _download_file(url, dest) + if not success: + return None + + # Make executable on Unix + if platform.system() != "Windows": + import stat + dest.chmod(dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + logger.info("p2pd_auto_download_done", path=str(dest)) + return dest + + except Exception as exc: + logger.warning("p2pd_auto_download_failed", error=str(exc)) + if dest.exists(): + dest.unlink() + return None diff --git a/network/cover_traffic.py b/network/cover_traffic.py new file mode 100644 index 0000000..4246263 --- /dev/null +++ b/network/cover_traffic.py @@ -0,0 +1,90 @@ +""" +Cover Traffic Manager — Phase 2 (Scaffold) + +Sends dummy encrypted packets at a constant rate to make real message traffic +indistinguishable from background noise. An observer watching any relay node +sees a constant stream of uniform-size packets regardless of real activity. + +Inspired by GNUnet's CADET anonymity model: + "All peers act as routers and use link-encrypted connections with stable + bandwidth utilization to communicate with each other." + +TODO Phase 2: Implement real cover traffic emission. +""" + +import asyncio +import os +from typing import Optional + +from utils.logging import get_logger + +logger = get_logger(__name__) + +# Default dummy packet size must match OnionRouter.pad_to_bytes +PACKET_SIZE_BYTES = 512 + +# Default cover traffic rate: 1 dummy packet per second per active peer connection. +# Configurable via RelayPreferences in a future update. +DEFAULT_RATE_HZ = 1.0 + + +class CoverTrafficManager: + """ + Emits dummy encrypted packets to camouflage real message timing. + + Phase 2 TODO: + - Select random peers from relay's known_peers list + - Encrypt a random payload with the peer's pubkey (indistinguishable from real traffic) + - Send at DEFAULT_RATE_HZ regardless of real message activity + - Uniform packet size matching OnionRouter.pad_to_bytes + """ + + def __init__(self, rate_hz: float = DEFAULT_RATE_HZ): + self.rate_hz = rate_hz + self._task: Optional[asyncio.Task] = None + self._running = False + + async def start(self, relay_node) -> None: + """ + Start emitting cover traffic. + + Args: + relay_node: RelayNodeManager instance to send dummy packets through. + + TODO Phase 2: implement. + """ + if self._running: + return + self._running = True + self._task = asyncio.create_task(self._emit_loop(relay_node)) + logger.info("cover_traffic_started", rate_hz=self.rate_hz) + + async def stop(self) -> None: + """Stop cover traffic emission.""" + self._running = False + if self._task: + self._task.cancel() + self._task = None + logger.info("cover_traffic_stopped") + + async def _emit_loop(self, relay_node) -> None: + """Emit dummy 512-byte packets at the configured rate to random online peers.""" + import random + + interval = 1.0 / self.rate_hz + while self._running: + try: + peers = [p for p in relay_node.get_peers() if p.get("online")] + if peers and relay_node._bridge and relay_node.is_running: + peer = random.choice(peers) + # Random-looking inbox topic derived from peer multiaddr hash + import hashlib + fake_id = hashlib.sha256( + peer["multiaddr"].encode() + str(int(asyncio.get_event_loop().time())).encode() + ).hexdigest()[:16] + dummy = os.urandom(PACKET_SIZE_BYTES) + await relay_node._bridge.publish(f"qube/{fake_id}/inbox", dummy) + logger.debug("cover_traffic_emitted", peer=peer["multiaddr"][:30]) + except Exception: + pass # Cover traffic failure must never affect real message delivery + await asyncio.sleep(interval) diff --git a/network/i2p_transport.py b/network/i2p_transport.py new file mode 100644 index 0000000..f339996 --- /dev/null +++ b/network/i2p_transport.py @@ -0,0 +1,190 @@ +""" +I2P Transport Driver — Phase 5 (Stub) + +Anonymous overlay network transport for users requiring maximum privacy. +Hides IP addresses from relay operators beyond what the onion layer provides. + +I2P provides: + - Garlic routing (multi-layer encryption, like onion routing but with bundles) + - Persistent pseudonymous addresses (I2P destinations / .b32.i2p addresses) + - Built-in NAT traversal (I2P handles its own hole punching) + - Stronger anonymity than Tor for I2P-native traffic + +Trade-offs vs direct TCP: + - Higher latency (typically 1–5 seconds for unidirectional, 2–10 for bidirectional) + - Lower throughput (shared I2P bandwidth) + - Requires i2pd or Java I2P router running on the host + +Dependencies: + Option A (recommended): i2pd C++ router (lightweight, ~20 MB) + Install: https://i2pd.website/en/download/ + Python bridge: pip install i2plib>=0.9.0 + + Option B: Java I2P router (full-featured, heavier) + Python bridge: SAMv3 API (I2P SAM bridge, TCP socket protocol) + + Option C: sam3 library + pip install sam3>=0.9.0 (implements SAMv3 directly) + +Addressing: + I2P destination: base32 or base64 encoded I2P address + Multiaddr format: /i2p/b32addr:XXXX.b32.i2p/p2p/QmPeerID + +Integration with relay: + Priority 5 in transport waterfall (lowest priority, used when user + explicitly enables I2P mode for maximum privacy). The relay protocol + does not change — I2P is just another transport driver. + +Privacy note: + When I2P transport is active, relay operators see only I2P destination + addresses (which are unlinkable to IP addresses), providing an additional + privacy layer on top of the onion routing already in Phase 2. + +TODO Phase 5: + 1. Detect i2pd or Java I2P router (check localhost:7657 for router console) + 2. Implement SAMv3 session creation via sam3 or i2plib + 3. implement discover_local_peers() via I2P netDB (bootstrap peers only) + 4. Wire into RelayNodeManager as lowest-priority optional transport + 5. UI toggle in Settings → Relay → "Use I2P for maximum privacy" +""" + +import asyncio +import struct +from typing import Any, Dict, List, Optional + +from utils.logging import get_logger + +logger = get_logger(__name__) + +CHUNK_SIZE = 32000 # I2P max datagram size +SCAN_TIMEOUT_SECS = 60.0 # I2P discovery is slow (netDB lookup) + +# SAMv3 bridge defaults (i2pd / Java I2P router) +SAM_HOST = "127.0.0.1" +SAM_PORT = 7656 + +try: + import sam3 + _SAM3_AVAILABLE = True +except ImportError: + try: + import i2plib + _I2PLIB_AVAILABLE = True + _SAM3_AVAILABLE = False + except ImportError: + _SAM3_AVAILABLE = False + _I2PLIB_AVAILABLE = False + +_I2P_AVAILABLE = _SAM3_AVAILABLE or locals().get("_I2PLIB_AVAILABLE", False) + + +class I2PTransport: + """ + I2P transport driver for Qubes relay — maximum privacy mode. + + Implements the 4-function relay transport interface over I2P. + Requires i2pd or Java I2P router + sam3 or i2plib Python bindings. + + Only used when explicitly enabled by the user via Settings → Relay. + Falls back gracefully if I2P router is not running. + """ + + def __init__(self) -> None: + if not _I2P_AVAILABLE: + raise ImportError( + "I2P transport requires an I2P router and Python bindings. " + "Install i2pd from https://i2pd.website and run: pip install sam3>=0.9.0" + ) + self._session: Optional[Any] = None # SAM3 session + self._destination: Optional[str] = None # Our I2P destination + self._receive_buffers: Dict[str, bytearray] = {} + self._receive_events: Dict[str, asyncio.Event] = {} + + async def start(self) -> None: + """ + Create a SAMv3 session with the local I2P router. + Generates an I2P destination (persistent pseudonymous address). + + TODO Phase 5: implement SAMv3 session creation. + """ + logger.info("i2p_session_start", sam_host=SAM_HOST, sam_port=SAM_PORT) + # TODO Phase 5: await sam3.SAMSession.create(SAM_HOST, SAM_PORT) + # self._destination = session.destination + + async def stop(self) -> None: + """Close the SAMv3 session.""" + if self._session: + # TODO Phase 5: await self._session.close() + self._session = None + logger.info("i2p_session_stopped") + + @property + def destination(self) -> Optional[str]: + """Our I2P destination address (.b32.i2p format).""" + return self._destination + + async def discover_local_peers(self) -> List[str]: + """ + Look up known Qubes relay I2P destinations from the I2P netDB. + Returns list of multiaddrs: ["/i2p/b32addr:.../p2p/...", ...] + + I2P has no mDNS equivalent — peers must be known in advance via + DHT or out-of-band exchange (normal relay list bootstrapping covers this). + + TODO Phase 5: resolve known relay I2P destinations from relay_list.json. + """ + logger.info("i2p_discover", note="using relay list, not netDB scan") + return [] + + async def connect(self, peer_multiaddr: str) -> Any: + """ + Open an I2P streaming connection to a peer. + + Args: + peer_multiaddr: "/i2p/b32addr:XXX.b32.i2p/p2p/..." format. + + Returns: + Stream handle for send/receive. + """ + parts = peer_multiaddr.split("/") + b32addr = "" + for part in parts: + if part.startswith("b32addr:"): + b32addr = part[8:] + break + if not b32addr: + raise ValueError(f"Cannot parse I2P destination from multiaddr: {peer_multiaddr}") + + logger.info("i2p_connect", dest=b32addr[:20]) + # TODO Phase 5: await session.connect(b32addr) + stream = {"dest": b32addr, "transport": self} + self._receive_buffers[b32addr] = bytearray() + self._receive_events[b32addr] = asyncio.Event() + return stream + + async def send(self, stream: Any, data: bytes) -> None: + """Send data over an I2P streaming connection.""" + dest = stream["dest"] + fragments = [data[i:i + CHUNK_SIZE] for i in range(0, len(data), CHUNK_SIZE)] + total = len(fragments) + for seq, fragment in enumerate(fragments): + header = struct.pack(">II", seq, total) + packet = header + fragment + # TODO Phase 5: write to SAM3 stream socket + logger.debug("i2p_send", dest=dest[:12], seq=seq, total=total) + + async def receive(self, stream: Any) -> bytes: + """Wait for and return the next complete message from an I2P stream.""" + dest = stream["dest"] + event = self._receive_events.get(dest) + if event: + await event.wait() + event.clear() + data = bytes(self._receive_buffers.get(dest, bytearray())) + self._receive_buffers[dest] = bytearray() + return data + + +def is_i2p_available() -> bool: + """Return True if I2P transport can be used on this system.""" + return _I2P_AVAILABLE diff --git a/network/libp2p_daemon_bridge.py b/network/libp2p_daemon_bridge.py index de74b68..dbeb622 100644 --- a/network/libp2p_daemon_bridge.py +++ b/network/libp2p_daemon_bridge.py @@ -113,13 +113,21 @@ async def start(self) -> None: logger.info("starting_libp2p_daemon", qube_id=self.qube_id) # Build daemon command + # WebSocket and QUIC ports derived from TCP port + ws_port = self.listen_port + 1 if self.listen_port else 4002 + quic_port = self.listen_port if self.listen_port else 4001 + cmd = [ self.daemon_binary, f"-listen=/ip4/0.0.0.0/tcp/{self.listen_port}", + f"-listen=/ip4/0.0.0.0/tcp/{ws_port}/ws", # WebSocket (browser + iOS) + f"-listen=/ip4/0.0.0.0/udp/{quic_port}/quic", # QUIC (mobile connection migration) f"-controlSocket={self.control_socket_path}", - "-dht", # Enable DHT - "-dhtClient=false", # Run as DHT server (provide content) - "-gossipsub", # Enable GossipSub + "-dht", # Enable Kademlia DHT + "-dhtClient=false", # Full DHT server (provide + route) + "-gossipsub", # Enable GossipSub pub/sub + "-natPortMap", # UPnP/NAT-PMP port mapping + "-autoRelay", # Act as circuit relay for NATed peers ] # Add bootstrap peers diff --git a/network/lora_transport.py b/network/lora_transport.py new file mode 100644 index 0000000..89d16a5 --- /dev/null +++ b/network/lora_transport.py @@ -0,0 +1,183 @@ +""" +LoRa Transport Driver — Phase 5 (Stub) + +Long-range radio transport for extreme offline scenarios. +Typical bandwidth: ~250 bytes/packet. Range: 1–15 km depending on hardware. + +Hardware requirements: + - LoRa radio module (e.g. RFM95, SX1276, RAK811) + - USB-serial adapter or GPIO connection to host + - Antenna matched to frequency (868 MHz EU / 915 MHz US / 433 MHz Asia) + +Python library options (choose one): + pip install pyserial>=3.5 # Direct AT-command serial (most hardware) + pip install RPi.GPIO lora # Raspberry Pi GPIO-connected modules + +Message design: + LoRa MTU is ~255 bytes raw. After LoRaWAN overhead: ~222 bytes. + We use the same [seq_4b][total_4b][data] framing as BLE transport. + With 222-byte payload capacity, a 512-byte message takes 3 fragments. + + At SF7 (fastest): ~6 kbps → 3 fragments ≈ 240ms + inter-packet delay. + At SF12 (longest range): ~290 bps → very slow; set MAX_PAYLOAD_BYTES = 51. + +Addressing: + Multiaddr format: /lora/DevEUI:0011223344556677/p2p/QmPeerID + DevEUI is the hardware EUI-64 of the LoRa module. + +Integration with relay: + This driver plugs into the transport fallback waterfall at Priority 5 + (lowest, after BLE). The relay protocol never changes — only the driver + needs to implement the 4-function interface. + +TODO Phase 5: + 1. Detect LoRa hardware (serial port enumeration) + 2. Implement serial AT-command driver for popular LoRa modules + 3. Implement discover_local_peers() via LoRa broadcast ping + 4. Fragment and reassemble messages within LoRa MTU constraints + 5. Add to RelayNodeManager transport list with lowest priority +""" + +import asyncio +import struct +from typing import Any, Callable, Dict, List, Optional + +from utils.logging import get_logger + +logger = get_logger(__name__) + +# LoRa payload limit (bytes) — conservative for maximum compatibility +MAX_PAYLOAD_BYTES = 200 +LORA_SCAN_TIMEOUT_SECS = 30.0 # LoRa peer discovery is slow + +try: + import serial + import serial.tools.list_ports + _SERIAL_AVAILABLE = True +except ImportError: + _SERIAL_AVAILABLE = False + + +class LoRaTransport: + """ + LoRa transport driver for Qubes relay. + + Implements the 4-function relay transport interface over long-range radio. + Requires serial-connected LoRa hardware and pyserial. + + Gracefully raises ImportError if pyserial not installed. + """ + + def __init__(self, serial_port: Optional[str] = None, baud_rate: int = 9600) -> None: + if not _SERIAL_AVAILABLE: + raise ImportError( + "pyserial is required for LoRa transport. Install with: pip install pyserial>=3.5" + ) + self.serial_port = serial_port # None = auto-detect + self.baud_rate = baud_rate + self._conn: Optional[Any] = None # serial.Serial instance + self._receive_buffers: Dict[str, bytearray] = {} + self._receive_events: Dict[str, asyncio.Event] = {} + + async def discover_local_peers(self) -> List[str]: + """ + Broadcast a ping over LoRa and collect responses. + Returns list of peer multiaddrs: ["/lora/DevEUI:.../p2p/...", ...] + + TODO: implement AT-command broadcast + response collection. + """ + logger.info("lora_scan_start", timeout=LORA_SCAN_TIMEOUT_SECS) + # TODO Phase 5: broadcast LoRa ping, collect responding DevEUIs + return [] + + async def connect(self, peer_multiaddr: str) -> Any: + """ + 'Connect' to a LoRa peer (set target DevEUI for outgoing packets). + + LoRa is connectionless — 'connect' just records the target address. + + Args: + peer_multiaddr: "/lora/DevEUI:0011223344556677/p2p/..." format. + + Returns: + A dict acting as the stream handle: {"deveui": "...", "transport": self} + """ + parts = peer_multiaddr.split("/") + deveui = "" + for part in parts: + if part.startswith("DevEUI:"): + deveui = part[7:] + break + if not deveui: + raise ValueError(f"Cannot parse LoRa DevEUI from multiaddr: {peer_multiaddr}") + + logger.info("lora_connect", deveui=deveui) + # TODO Phase 5: open serial port if not already open + stream = {"deveui": deveui, "transport": self} + self._receive_buffers[deveui] = bytearray() + self._receive_events[deveui] = asyncio.Event() + return stream + + async def send(self, stream: Any, data: bytes) -> None: + """ + Send data to a LoRa peer, fragmented to MAX_PAYLOAD_BYTES. + + Args: + stream: Dict returned by connect() with "deveui" key. + data: Raw bytes to send. + """ + deveui = stream["deveui"] + fragments = [data[i:i + MAX_PAYLOAD_BYTES] for i in range(0, len(data), MAX_PAYLOAD_BYTES)] + total = len(fragments) + for seq, fragment in enumerate(fragments): + header = struct.pack(">II", seq, total) + packet = header + fragment + # TODO Phase 5: write packet to serial port using AT+SEND command + logger.debug("lora_send_fragment", deveui=deveui[:12], seq=seq, total=total, bytes=len(fragment)) + + async def receive(self, stream: Any) -> bytes: + """ + Wait for and return the next complete message from a LoRa stream. + + Args: + stream: Dict returned by connect(). + + Returns: + Reassembled message bytes. + """ + deveui = stream["deveui"] + event = self._receive_events.get(deveui) + if event: + await event.wait() + event.clear() + data = bytes(self._receive_buffers.get(deveui, bytearray())) + self._receive_buffers[deveui] = bytearray() + return data + + def _on_serial_data(self, raw_packet: bytes, sender_deveui: str) -> None: + """ + Called by serial read loop when a LoRa packet arrives. + Reassembles fragments and signals completion. + + TODO Phase 5: wire to serial read thread. + """ + try: + if len(raw_packet) < 8: + return + seq, total = struct.unpack_from(">II", raw_packet, 0) + payload = raw_packet[8:] + if sender_deveui not in self._receive_buffers: + self._receive_buffers[sender_deveui] = bytearray() + self._receive_events[sender_deveui] = asyncio.Event() + self._receive_buffers[sender_deveui].extend(payload) + if seq == total - 1: + event = self._receive_events.get(sender_deveui) + if event: + event.set() + except Exception as exc: + logger.debug("lora_packet_error", error=str(exc)) + + +def is_lora_available() -> bool: + """Return True if pyserial is installed and LoRa transport can be used.""" + return _SERIAL_AVAILABLE diff --git a/network/nostr_transport.py b/network/nostr_transport.py new file mode 100644 index 0000000..9bb380f --- /dev/null +++ b/network/nostr_transport.py @@ -0,0 +1,423 @@ +""" +Nostr Transport — Phase 3 + +Uses public Nostr relays as a Priority-4 transport fallback when the DHT +is unreachable (no internet, ISP blocks libp2p ports, etc.). + +Privacy design (BitChat-style): + - Ephemeral keypair per session — NOT the user's BCH wallet keypair. + The Nostr identity is mathematically unlinked from the BCH / Qube identity. + - NIP-44 v2 encrypted DMs (XChaCha20-Poly1305 + secp256k1 ECDH). + - Broadcast to all configured Nostr relays simultaneously. + - Recipient identified by a session-derived ephemeral pubkey known only + to the sender (derived via HKDF from qube_privkey + session context). + - Observer sees encrypted blobs from disposable ephemeral pubkeys — + no link to real BCH/Qube identities. + +Nostr relay list comes from EndpointPreferences.nostr_relays (Settings → Endpoints). +""" + +import asyncio +import base64 +import hashlib +import json +import os +import time +from typing import Any, Callable, Dict, List, Optional, Tuple + +from utils.logging import get_logger + +logger = get_logger(__name__) + +# --------------------------------------------------------------------------- +# secp256k1 curve constants +# --------------------------------------------------------------------------- +_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +_G = ( + 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, + 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, +) + + +def _point_add(P: Optional[tuple], Q: Optional[tuple]) -> Optional[tuple]: + if P is None: + return Q + if Q is None: + return P + if P[0] == Q[0]: + if P[1] != Q[1]: + return None + lam = (3 * P[0] * P[0] * pow(2 * P[1], _P - 2, _P)) % _P + else: + lam = ((Q[1] - P[1]) * pow(Q[0] - P[0], _P - 2, _P)) % _P + x = (lam * lam - P[0] - Q[0]) % _P + y = (lam * (P[0] - x) - P[1]) % _P + return (x, y) + + +def _point_mul(P: tuple, k: int) -> Optional[tuple]: + R: Optional[tuple] = None + while k: + if k & 1: + R = _point_add(R, P) + P = _point_add(P, P) + k >>= 1 + return R + + +def _lift_x(x: int) -> tuple: + """Lift x-coordinate to (x, y) secp256k1 point with even y.""" + y_sq = (pow(x, 3, _P) + 7) % _P + y = pow(y_sq, (_P + 1) // 4, _P) + if (y * y) % _P != y_sq: + raise ValueError("Point not on secp256k1 curve") + return (x, y if y % 2 == 0 else _P - y) + + +def _tagged_hash(tag: str, data: bytes) -> bytes: + h = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(h + h + data).digest() + + +def _privkey_to_xonly_pubkey(d: int) -> bytes: + """Return 32-byte x-only pubkey from integer private key.""" + P = _point_mul(_G, d) + return P[0].to_bytes(32, "big") + + +def _pubkey_bytes_to_point(pubkey: bytes) -> tuple: + """ + Accept 32-byte x-only (BIP-340) or 33-byte compressed pubkey. + Returns (x, y) curve point. + """ + if len(pubkey) == 32: + return _lift_x(int.from_bytes(pubkey, "big")) + if len(pubkey) == 33: + x = int.from_bytes(pubkey[1:], "big") + y_sq = (pow(x, 3, _P) + 7) % _P + y = pow(y_sq, (_P + 1) // 4, _P) + if (y * y) % _P != y_sq: + raise ValueError("Compressed pubkey not on curve") + want_even = pubkey[0] == 0x02 + if (y % 2 == 0) != want_even: + y = _P - y + return (x, y) + raise ValueError(f"Unsupported pubkey length: {len(pubkey)}") + + +# --------------------------------------------------------------------------- +# BIP-340 Schnorr signing (needed for valid Nostr events) +# --------------------------------------------------------------------------- + +def _schnorr_sign(msg32: bytes, secret_key_int: int) -> bytes: + """ + BIP-340 Schnorr signature. + msg32 must be exactly 32 bytes (the SHA-256 event ID). + """ + assert len(msg32) == 32 + P = _point_mul(_G, secret_key_int) + # Ensure public key has even y + d = (_N - secret_key_int) if P[1] % 2 != 0 else secret_key_int + px = P[0].to_bytes(32, "big") + + # Deterministic nonce via BIP-340/nonce tagged hash + a = d.to_bytes(32, "big") + rand = _tagged_hash("BIP0340/nonce", a + px + msg32) + k0 = int.from_bytes(rand, "big") % _N + if k0 == 0: + raise ValueError("Schnorr nonce is zero — this should never happen") + + R = _point_mul(_G, k0) + k = (_N - k0) if R[1] % 2 != 0 else k0 + rx = R[0].to_bytes(32, "big") + + e = int.from_bytes(_tagged_hash("BIP0340/challenge", rx + px + msg32), "big") % _N + s = (k + e * d) % _N + return rx + s.to_bytes(32, "big") + + +# --------------------------------------------------------------------------- +# NIP-44 v2 encryption +# --------------------------------------------------------------------------- + +def _nip44_conversation_key(our_privkey_int: int, their_pubkey_point: tuple) -> bytes: + """ECDH + HKDF-SHA256 to derive a 32-byte NIP-44 conversation key.""" + from cryptography.hazmat.primitives.kdf.hkdf import HKDF + from cryptography.hazmat.primitives import hashes + + shared_point = _point_mul(their_pubkey_point, our_privkey_int) + shared_x = shared_point[0].to_bytes(32, "big") + + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=b"nip44-v2", + info=b"", + ) + return hkdf.derive(shared_x) + + +def _nip44_encrypt(plaintext: bytes, conversation_key: bytes) -> str: + """ + Encrypt plaintext with NIP-44 v2. + Uses XChaCha20-Poly1305 (24-byte nonce) when available in `cryptography`, + falls back to ChaCha20-Poly1305 (12-byte nonce) for older builds. + Both ends use the same version byte so decryption is symmetric. + """ + try: + from cryptography.hazmat.primitives.ciphers.aead import XChaCha20Poly1305 + nonce = os.urandom(24) + ct = XChaCha20Poly1305(conversation_key).encrypt(nonce, plaintext, None) + raw = b"\x02" + nonce + ct # version=2 (NIP-44 v2) + except (ImportError, AttributeError): + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + nonce = os.urandom(12) + ct = ChaCha20Poly1305(conversation_key).encrypt(nonce, plaintext, None) + raw = b"\x01" + nonce + ct # version=1 (Qubes fallback) + return base64.b64encode(raw).decode() + + +def _nip44_decrypt(ciphertext_b64: str, conversation_key: bytes) -> bytes: + """Decrypt NIP-44 ciphertext (version 1 or 2).""" + raw = base64.b64decode(ciphertext_b64) + version = raw[0] + if version == 0x02: + from cryptography.hazmat.primitives.ciphers.aead import XChaCha20Poly1305 + return XChaCha20Poly1305(conversation_key).decrypt(raw[1:25], raw[25:], None) + if version == 0x01: + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + return ChaCha20Poly1305(conversation_key).decrypt(raw[1:13], raw[13:], None) + raise ValueError(f"Unsupported NIP-44 version byte: {version}") + + +# --------------------------------------------------------------------------- +# Nostr event helpers +# --------------------------------------------------------------------------- + +def _event_id(pubkey_hex: str, created_at: int, kind: int, tags: list, content: str) -> str: + serialized = json.dumps( + [0, pubkey_hex, created_at, kind, tags, content], + separators=(",", ":"), + ensure_ascii=False, + ).encode() + return hashlib.sha256(serialized).hexdigest() + + +# --------------------------------------------------------------------------- +# NostrTransport +# --------------------------------------------------------------------------- + +class NostrTransport: + """ + Nostr relay transport with NIP-44 + ephemeral keypairs. + Used as Priority-4 fallback in the relay send waterfall. + """ + + def __init__(self, relay_urls: List[str]): + """ + Args: + relay_urls: Nostr relay WebSocket URLs from EndpointPreferences.nostr_relays. + """ + self.relay_urls = relay_urls + self._running = False + self._listen_tasks: List[asyncio.Task] = [] + + async def start(self) -> None: + """Mark transport as active. Connections are opened lazily.""" + self._running = True + logger.info("nostr_transport_started", relay_count=len(self.relay_urls)) + + async def stop(self) -> None: + """Cancel all listeners and mark stopped.""" + self._running = False + for task in self._listen_tasks: + task.cancel() + self._listen_tasks.clear() + logger.info("nostr_transport_stopped") + + async def send( + self, + recipient_ephemeral_pubkey_hex: str, + encrypted_payload: bytes, + our_privkey_int: Optional[int] = None, + ) -> bool: + """ + Publish a NIP-44 encrypted DM (kind:4) to all configured relays. + + Args: + recipient_ephemeral_pubkey_hex: 32-byte x-only or 33-byte compressed pubkey hex. + encrypted_payload: Pre-encrypted message bytes (from messaging.py ECIES). + our_privkey_int: Our ephemeral private key as integer. Required. + + Returns: + True if at least one relay accepted the event. + """ + if not self.relay_urls or our_privkey_int is None: + return False + + try: + our_pubkey_bytes = _privkey_to_xonly_pubkey(our_privkey_int) + our_pubkey_hex = our_pubkey_bytes.hex() + + rec_bytes = bytes.fromhex(recipient_ephemeral_pubkey_hex) + rec_point = _pubkey_bytes_to_point(rec_bytes) + + # NIP-44 encrypt + conv_key = _nip44_conversation_key(our_privkey_int, rec_point) + ciphertext = _nip44_encrypt(encrypted_payload, conv_key) + + # Recipient as 32-byte x-only hex for the "p" tag + rec_xonly = (rec_bytes if len(rec_bytes) == 32 else rec_bytes[1:]).hex() + + created_at = int(time.time()) + kind = 4 + tags: List[List[str]] = [["p", rec_xonly]] + content = ciphertext + + eid = _event_id(our_pubkey_hex, created_at, kind, tags, content) + sig_bytes = _schnorr_sign(bytes.fromhex(eid), our_privkey_int) + + event: Dict[str, Any] = { + "id": eid, + "pubkey": our_pubkey_hex, + "created_at": created_at, + "kind": kind, + "tags": tags, + "content": content, + "sig": sig_bytes.hex(), + } + msg = json.dumps(["EVENT", event]) + + results = await asyncio.gather( + *[self._send_to_relay(url, msg) for url in self.relay_urls], + return_exceptions=True, + ) + success = any(r is True for r in results) + logger.info( + "nostr_send_done", + success=success, + relays=len(self.relay_urls), + payload_len=len(encrypted_payload), + ) + return success + + except Exception as exc: + logger.warning("nostr_send_error", error=str(exc)) + return False + + async def _send_to_relay(self, url: str, msg: str) -> bool: + """Send a single EVENT to one relay, return True if accepted.""" + try: + import websockets + async with websockets.connect(url, open_timeout=6, close_timeout=2) as ws: + await ws.send(msg) + try: + resp_raw = await asyncio.wait_for(ws.recv(), timeout=5) + data = json.loads(resp_raw) + # NIP-01: ["OK", , , ] + if isinstance(data, list) and data[0] == "OK" and len(data) >= 3: + return bool(data[2]) + except Exception: + pass + return False + except Exception as exc: + logger.debug("nostr_relay_unavailable", url=url[:30], error=str(exc)) + return False + + async def listen( + self, + my_ephemeral_privkey_int: int, + handler: Callable[[bytes], None], + ) -> None: + """ + Subscribe to incoming kind:4 messages on all relays. + Spawns background tasks that reconnect on disconnect. + + Args: + my_ephemeral_privkey_int: Our ephemeral private key integer. + handler: Called with decrypted payload bytes for each received message. + """ + my_pubkey_hex = _privkey_to_xonly_pubkey(my_ephemeral_privkey_int).hex() + for url in self.relay_urls: + task = asyncio.create_task( + self._listen_relay(url, my_ephemeral_privkey_int, my_pubkey_hex, handler) + ) + self._listen_tasks.append(task) + logger.info("nostr_listen_started", relays=len(self.relay_urls), pubkey=my_pubkey_hex[:12]) + + async def _listen_relay( + self, + url: str, + my_privkey_int: int, + my_pubkey_hex: str, + handler: Callable[[bytes], None], + ) -> None: + sub_id = f"qubes-{my_pubkey_hex[:8]}" + req = json.dumps(["REQ", sub_id, {"kinds": [4], "#p": [my_pubkey_hex]}]) + + while self._running: + try: + import websockets + async with websockets.connect(url, open_timeout=6, close_timeout=2) as ws: + await ws.send(req) + async for raw in ws: + if not self._running: + break + try: + data = json.loads(raw) + if ( + isinstance(data, list) + and data[0] == "EVENT" + and data[1] == sub_id + ): + event = data[2] + sender_point = _pubkey_bytes_to_point( + bytes.fromhex(event["pubkey"]) + ) + conv_key = _nip44_conversation_key(my_privkey_int, sender_point) + plaintext = _nip44_decrypt(event["content"], conv_key) + handler(plaintext) + except Exception as exc: + logger.debug("nostr_event_decode_error", error=str(exc)) + except Exception as exc: + logger.debug("nostr_relay_disconnected", url=url[:30], error=str(exc)) + if self._running: + await asyncio.sleep(10) + + @staticmethod + def derive_session_keypair( + qube_privkey_bytes: bytes, + recipient_qube_id: str, + session_id: str, + ) -> Tuple[bytes, bytes]: + """ + Derive an ephemeral (privkey_bytes, xonly_pubkey_bytes) pair for a session. + + HKDF-SHA256 ensures the Nostr keypair is deterministic per session but + completely unlinkable from the BCH wallet keypair by outside observers. + + Args: + qube_privkey_bytes: Raw 32-byte Qube secp256k1 private key. + recipient_qube_id: Recipient's Qube ID (8-char token prefix). + session_id: Unique session identifier (e.g. conversation_id UUID). + + Returns: + (ephemeral_privkey_32_bytes, ephemeral_xonly_pubkey_32_bytes) + """ + from cryptography.hazmat.primitives.kdf.hkdf import HKDF + from cryptography.hazmat.primitives import hashes + + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=b"qubes-nostr-v1", + info=f"session:{recipient_qube_id}:{session_id}".encode(), + ) + raw = hkdf.derive(qube_privkey_bytes) + + # Clamp to valid secp256k1 scalar range [1, N-1] + d = int.from_bytes(raw, "big") % (_N - 1) + 1 + privkey_bytes = d.to_bytes(32, "big") + pubkey_bytes = _privkey_to_xonly_pubkey(d) + return privkey_bytes, pubkey_bytes diff --git a/network/onion.py b/network/onion.py new file mode 100644 index 0000000..e64616a --- /dev/null +++ b/network/onion.py @@ -0,0 +1,257 @@ +""" +Onion Router — Phase 2 + +2-hop onion routing to hide the communication graph from relay operators. +When Alice sends to Bob, she picks 2 intermediate relays (R1, R2) and wraps +the payload in 3 ECIES encryption layers: + + Outer → encrypted for R1: { "next_hop": r2_relay_addr, "payload": } + Middle → encrypted for R2: { "next_hop": dest_relay_addr, "payload": } + Inner → encrypted for dest: { "payload": } + +R1 learns only: "forward to R2". +R2 learns only: "forward to Bob's relay". +Neither learns Alice is the origin. + +Encryption: ephemeral ECDH (secp256k1) → HKDF-SHA256 → AES-256-GCM. +Packet size: padded to uniform PAD_BYTES (512) to prevent size-based traffic analysis. +""" + +import json +import os +from typing import Optional, Tuple + +from utils.logging import get_logger + +logger = get_logger(__name__) + +PAD_BYTES = 512 + + +# --------------------------------------------------------------------------- +# ECIES helpers — ephemeral ECDH + AES-256-GCM +# Reuses the cryptography library already in requirements.txt. +# --------------------------------------------------------------------------- + +def _ecies_encrypt(plaintext: bytes, recipient_pubkey_bytes: bytes) -> bytes: + """ + Encrypt plaintext for recipient_pubkey using ephemeral ECDH + AES-256-GCM. + + Output wire format: + ephemeral_pubkey (33 bytes, compressed) + nonce (12 bytes) + ciphertext + tag (len(plaintext) + 16 bytes) + + Args: + plaintext: Bytes to encrypt. + recipient_pubkey_bytes: 33-byte compressed or 32-byte x-only secp256k1 pubkey. + """ + from cryptography.hazmat.primitives.asymmetric.ec import ( + SECP256K1, + generate_private_key, + ECDH, + ) + from cryptography.hazmat.primitives.kdf.hkdf import HKDF + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + + # Load recipient public key + if len(recipient_pubkey_bytes) == 32: + # x-only: reconstruct compressed (even y = 02 prefix) + recipient_pubkey_bytes = b"\x02" + recipient_pubkey_bytes + + from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey + from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + from cryptography.hazmat.primitives.asymmetric.ec import ( + EllipticCurvePublicNumbers, + ) + from cryptography.hazmat.backends import default_backend + + recipient_pub = EllipticCurvePublicKey.from_encoded_point( + SECP256K1(), recipient_pubkey_bytes + ) + + # Generate ephemeral keypair + ephemeral_priv = generate_private_key(SECP256K1()) + ephemeral_pub_bytes = ephemeral_priv.public_key().public_bytes( + Encoding.X962, PublicFormat.CompressedPoint + ) + + # ECDH → shared secret + shared_secret = ephemeral_priv.exchange(ECDH(), recipient_pub) + + # HKDF-SHA256 → 32-byte AES key + aes_key = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=b"qubes-onion-v1", + ).derive(shared_secret) + + # AES-256-GCM encrypt + nonce = os.urandom(12) + ciphertext = AESGCM(aes_key).encrypt(nonce, plaintext, None) + + return ephemeral_pub_bytes + nonce + ciphertext + + +def _ecies_decrypt(ciphertext_blob: bytes, my_privkey_bytes: bytes) -> bytes: + """ + Decrypt an ECIES blob produced by _ecies_encrypt. + + Args: + ciphertext_blob: Wire-format bytes (see _ecies_encrypt). + my_privkey_bytes: 32-byte raw private key scalar. + """ + from cryptography.hazmat.primitives.asymmetric.ec import ( + SECP256K1, + ECDH, + EllipticCurvePublicKey, + ) + from cryptography.hazmat.primitives.kdf.hkdf import HKDF + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + + # Parse wire format + ephemeral_pub_bytes = ciphertext_blob[:33] + nonce = ciphertext_blob[33:45] + ciphertext = ciphertext_blob[45:] + + ephemeral_pub = EllipticCurvePublicKey.from_encoded_point( + SECP256K1(), ephemeral_pub_bytes + ) + + # Reconstruct private key + from cryptography.hazmat.primitives.asymmetric.ec import ( + derive_private_key, + ) + d = int.from_bytes(my_privkey_bytes, "big") + my_priv = derive_private_key(d, SECP256K1()) + + shared_secret = my_priv.exchange(ECDH(), ephemeral_pub) + + aes_key = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=b"qubes-onion-v1", + ).derive(shared_secret) + + return AESGCM(aes_key).decrypt(nonce, ciphertext, None) + + +# --------------------------------------------------------------------------- +# OnionRouter +# --------------------------------------------------------------------------- + +class OnionRouter: + """ + 2-hop onion routing for Qubes P2P relay. + + Used by RelayNodeManager.send_message() before dispatching to the DHT. + Each hop peels one ECIES layer and forwards the inner packet. + Packets are padded to PAD_BYTES (512) for uniform size. + """ + + def __init__(self, pad_to_bytes: int = PAD_BYTES): + self.pad_to_bytes = pad_to_bytes + + async def wrap_message( + self, + payload: bytes, + r1_pubkey: bytes, + r2_pubkey: bytes, + dest_pubkey: bytes, + dest_relay_addr: str, + r2_relay_addr: str, + ) -> bytes: + """ + Wrap payload in 3 ECIES layers (outer → R1, middle → R2, inner → dest). + + Args: + payload: Already encrypted message bytes (from messaging.py). + r1_pubkey: First hop's secp256k1 pubkey (33 bytes compressed or 32 x-only). + r2_pubkey: Second hop's secp256k1 pubkey. + dest_pubkey: Final recipient's secp256k1 pubkey. + dest_relay_addr: Multiaddr of recipient's relay (R2 forwards here). + r2_relay_addr: Multiaddr of R2 (R1 forwards here). + + Returns: + Padded onion packet bytes ready for transmission. + """ + try: + # Inner layer: encrypted for dest, contains the actual payload + inner_json = json.dumps({"payload": payload.hex()}).encode() + inner = _ecies_encrypt(inner_json, dest_pubkey) + + # Middle layer: encrypted for R2, contains dest_relay_addr + inner + middle_json = json.dumps({ + "next_hop": dest_relay_addr, + "payload": inner.hex(), + }).encode() + middle = _ecies_encrypt(middle_json, r2_pubkey) + + # Outer layer: encrypted for R1, contains r2_relay_addr + middle + outer_json = json.dumps({ + "next_hop": r2_relay_addr, + "payload": middle.hex(), + }).encode() + outer = _ecies_encrypt(outer_json, r1_pubkey) + + padded = self._pad(outer) + logger.debug( + "onion_wrap_done", + original_len=len(payload), + onion_len=len(padded), + ) + return padded + + except Exception as exc: + # Never let onion failure block message delivery — fall back to raw payload + logger.warning("onion_wrap_failed", error=str(exc)) + return payload + + async def unwrap_message( + self, + onion_packet: bytes, + my_privkey: bytes, + ) -> Tuple[bytes, Optional[str]]: + """ + Peel one ECIES layer. + + Args: + onion_packet: Received onion bytes (may be padded). + my_privkey: This node's 32-byte private key. + + Returns: + (inner_payload_bytes, next_hop_addr) + If next_hop_addr is None, this node is the final recipient. + """ + try: + # Strip trailing padding zeros (the ECIES magic bytes are non-zero) + stripped = onion_packet.rstrip(b"\x00") if len(onion_packet) == self.pad_to_bytes else onion_packet + + decrypted = _ecies_decrypt(stripped, my_privkey) + layer = json.loads(decrypted.decode()) + + inner_hex = layer.get("payload", "") + inner_bytes = bytes.fromhex(inner_hex) + next_hop = layer.get("next_hop") # None if this is the innermost layer + + logger.debug( + "onion_unwrap_done", + next_hop=next_hop, + inner_len=len(inner_bytes), + ) + return inner_bytes, next_hop + + except Exception as exc: + logger.warning("onion_unwrap_failed", error=str(exc)) + # Return packet as-is — caller handles delivery + return onion_packet, None + + def _pad(self, data: bytes) -> bytes: + """Pad data to uniform packet size to prevent size-based traffic analysis.""" + if len(data) < self.pad_to_bytes: + return data + b"\x00" * (self.pad_to_bytes - len(data)) + return data diff --git a/network/relay_list.py b/network/relay_list.py new file mode 100644 index 0000000..85336d4 --- /dev/null +++ b/network/relay_list.py @@ -0,0 +1,83 @@ +""" +Built-in Relay List + +Pre-loaded list of seed relays, following the same pattern as Electron Cash's +built-in Fulcrum server list. As the community grows, more relays will be added. + +Users can add/remove any relay via Settings → Relay Node → Custom Relays. +Full user control — even seed relays can be removed. +""" + +from typing import List, Dict, Any + + +# BitFaced-operated seed relays (always present, maintained long-term). +# BitFaced fills in real multiaddrs and peer IDs before deploying. +BUILTIN_RELAYS: List[Dict[str, Any]] = [ + # ------------------------------------------------------------------------- + # BitFaced Seed Relays (official) + # ------------------------------------------------------------------------- + { + "id": "QmRelayUS", + "label": "BitFaced Relay US-East", + "operator": "BitFaced", + "region": "US-East", + "multiaddrs": [ + "/ip4/relay-us.qube.cash/tcp/4001/p2p/QmRelayUS", + "/ip4/relay-us.qube.cash/tcp/443/wss/p2p/QmRelayUS", + ], + }, + { + "id": "QmRelayEU", + "label": "BitFaced Relay EU", + "operator": "BitFaced", + "region": "EU", + "multiaddrs": [ + "/ip4/relay-eu.qube.cash/tcp/4001/p2p/QmRelayEU", + "/ip4/relay-eu.qube.cash/tcp/443/wss/p2p/QmRelayEU", + ], + }, + { + "id": "QmRelayAS", + "label": "BitFaced Relay Asia", + "operator": "BitFaced", + "region": "Asia", + "multiaddrs": [ + "/ip4/relay-as.qube.cash/tcp/4001/p2p/QmRelayAS", + "/ip4/relay-as.qube.cash/tcp/443/wss/p2p/QmRelayAS", + ], + }, + # ------------------------------------------------------------------------- + # Community Relay Slots + # Open an issue with label 'relay-listing' to get your relay added here. + # Requirements: 30+ days uptime, known operator, open to all Qubes users. + # ------------------------------------------------------------------------- + # { + # "id": "QmCommRelay1", + # "label": "Community Relay 1", + # "operator": "TBD", + # "region": "TBD", + # "multiaddrs": [], + # }, +] + + +def get_all_bootstrap_multiaddrs() -> List[str]: + """Return flat list of all multiaddrs from the built-in relay list.""" + addrs = [] + for relay in BUILTIN_RELAYS: + addrs.extend(relay["multiaddrs"]) + return addrs + + +def get_relay_by_id(relay_id: str) -> Dict[str, Any] | None: + """Look up a built-in relay by its ID.""" + for relay in BUILTIN_RELAYS: + if relay["id"] == relay_id: + return relay + return None + + +def get_relay_labels() -> List[str]: + """Return human-readable labels for all built-in relays.""" + return [r["label"] for r in BUILTIN_RELAYS] diff --git a/network/relay_node.py b/network/relay_node.py new file mode 100644 index 0000000..a88bbd1 --- /dev/null +++ b/network/relay_node.py @@ -0,0 +1,393 @@ +""" +Relay Node Manager — Phase 1 + +High-level manager for the Qubes P2P relay node. Wraps QubeP2PNode and +LibP2PDaemonBridge, loads the built-in relay list, manages custom peers, +and exposes a clean interface to gui_bridge.py. + +Transport fallback waterfall (Priority 1 → 5): + 1. Direct local relay (this node) + 2. Internet DHT routing (Kademlia via p2pd) + 3. Nostr relay fallback (Phase 3 — see nostr_transport.py) + 4. BLE / LoRa mesh (Phase 6) +""" + +import asyncio +import json +import time +from pathlib import Path +from typing import Any, Dict, List, Optional + +from network.p2p_node import QubeP2PNode +from network.libp2p_daemon_bridge import LibP2PDaemonBridge +from network.relay_list import BUILTIN_RELAYS, get_all_bootstrap_multiaddrs +from network.store_forward import StoreForwardQueue +from network.onion import OnionRouter +from network.cover_traffic import CoverTrafficManager +from network.nostr_transport import NostrTransport +from network.bundle_manager import ensure_p2pd_binary +from utils.logging import get_logger + +logger = get_logger(__name__) + + +class RelayNodeManager: + """ + High-level P2P relay node for Qubes. + + Manages: + - Local p2pd daemon lifecycle (start / stop) + - Built-in + user-configured seed peers + - Outbound message sending via DHT routing + - Inbound store-and-forward message delivery + - Peer reachability polling (for UI status dots) + """ + + def __init__( + self, + user_data_dir: Path, + listen_port: int = 0, + max_connections: int = 50, + retention_days: int = 7, + p2pd_binary: Optional[str] = None, + custom_peers: Optional[List[str]] = None, + nostr_relay_urls: Optional[List[str]] = None, + ): + self.user_data_dir = Path(user_data_dir) + self.listen_port = listen_port + self.max_connections = max_connections + self.retention_days = retention_days + self.p2pd_binary = p2pd_binary # None = use bundled binary + + # Built-in + user custom peers + bootstrap = get_all_bootstrap_multiaddrs() + if custom_peers: + bootstrap.extend(custom_peers) + self.bootstrap_peers = bootstrap + self.custom_peers: List[str] = list(custom_peers or []) + + # p2pd bridge (initialised in start()) + self._bridge: Optional[LibP2PDaemonBridge] = None + self._p2p_node: Optional[QubeP2PNode] = None + + # Store-and-forward queue + queue_dir = self.user_data_dir / "relay_queue" + queue_dir.mkdir(parents=True, exist_ok=True) + self.store_forward = StoreForwardQueue(queue_dir, retention_days=retention_days) + + # Runtime state + self.is_running = False + self.peer_id: Optional[str] = None + self.multiaddr: Optional[str] = None + + # Peer reachability cache: multiaddr → {online, latency_ms, last_checked} + self._peer_status: Dict[str, Dict[str, Any]] = {} + self._poll_task: Optional[asyncio.Task] = None + + # Phase 2: onion routing + cover traffic + self._onion = OnionRouter() + self._cover = CoverTrafficManager() + + # Phase 2: peer pubkey cache — populated as peers connect: peer_id → pubkey_bytes + self._peer_pubkeys: Dict[str, bytes] = {} + + # Phase 3: Nostr fallback transport — relay_urls from EndpointPreferences + _nostr_urls = list(nostr_relay_urls) if nostr_relay_urls else [] + self._nostr: Optional[NostrTransport] = NostrTransport(_nostr_urls) if _nostr_urls else None + self._nostr_privkey_int: Optional[int] = None # ephemeral privkey; set in start() + + logger.info("relay_node_manager_initialized", listen_port=listen_port) + + # ------------------------------------------------------------------------- + # Lifecycle + # ------------------------------------------------------------------------- + + async def start(self) -> None: + """Start the local relay daemon and connect to bootstrap peers.""" + if self.is_running: + return + + try: + # Auto-download p2pd binary if not present + if not self.p2pd_binary: + downloaded = await ensure_p2pd_binary() + if downloaded: + self.p2pd_binary = str(downloaded) + + self._bridge = LibP2PDaemonBridge( + qube_id="relay", + listen_port=self.listen_port, + bootstrap_peers=self.bootstrap_peers, + daemon_binary=self.p2pd_binary or "p2pd", + ) + await self._bridge.start() + + self.peer_id = getattr(self._bridge, "peer_id", None) + self.multiaddr = getattr(self._bridge, "multiaddr", None) + self.is_running = True + + # Purge expired messages from the queue on startup + self.store_forward.purge_expired() + + # Start background peer polling (every 30 s) + self._poll_task = asyncio.create_task(self._poll_peers_loop()) + + # Phase 2: start cover traffic + await self._cover.start(self) + + # Phase 3: generate ephemeral Nostr privkey for this session + import os as _os + _raw = _os.urandom(32) + _N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + self._nostr_privkey_int = int.from_bytes(_raw, "big") % (_N - 1) + 1 + + # Start Nostr transport if relay URLs were configured + if self._nostr: + await self._nostr.start() + + logger.info("relay_node_started", peer_id=self.peer_id, multiaddr=self.multiaddr) + + except Exception as exc: + self.is_running = False + logger.warning("relay_node_start_failed", error=str(exc)) + raise + + async def stop(self) -> None: + """Stop the relay daemon, cover traffic, and Nostr transport.""" + if self._poll_task: + self._poll_task.cancel() + self._poll_task = None + + # Phase 2: stop cover traffic + await self._cover.stop() + + # Phase 3: stop Nostr transport + if self._nostr: + await self._nostr.stop() + + if self._bridge: + try: + await self._bridge.stop() + except Exception: + pass + self._bridge = None + + self.is_running = False + self.peer_id = None + self.multiaddr = None + logger.info("relay_node_stopped") + + # ------------------------------------------------------------------------- + # Status + # ------------------------------------------------------------------------- + + def get_status(self) -> Dict[str, Any]: + """Return current relay node status for the Settings UI.""" + return { + "running": self.is_running, + "peer_id": self.peer_id, + "multiaddr": self.multiaddr, + "peer_count": len(self._peer_status), + "online_peers": sum(1 for p in self._peer_status.values() if p.get("online")), + } + + # ------------------------------------------------------------------------- + # Peer management + # ------------------------------------------------------------------------- + + def get_peers(self) -> List[Dict[str, Any]]: + """ + Return combined peer list: + - Built-in seed relays (read-only, with live status) + - User custom peers (removable, with live status) + """ + peers = [] + + for relay in BUILTIN_RELAYS: + for addr in relay["multiaddrs"]: + status = self._peer_status.get(addr, {}) + peers.append({ + "multiaddr": addr, + "label": relay["label"], + "operator": relay["operator"], + "builtin": True, + "online": status.get("online", None), # None = not yet checked + "latency_ms": status.get("latency_ms", None), + }) + + for addr in self.custom_peers: + status = self._peer_status.get(addr, {}) + peers.append({ + "multiaddr": addr, + "label": addr, + "operator": "custom", + "builtin": False, + "online": status.get("online", None), + "latency_ms": status.get("latency_ms", None), + }) + + return peers + + async def add_peer(self, multiaddr: str) -> bool: + """Add a custom relay peer.""" + if multiaddr in self.custom_peers: + return False + self.custom_peers.append(multiaddr) + if self._bridge: + try: + await self._bridge.connect(multiaddr) + except Exception: + pass + logger.info("relay_peer_added", multiaddr=multiaddr) + return True + + async def remove_peer(self, multiaddr: str) -> bool: + """Remove a custom relay peer.""" + if multiaddr not in self.custom_peers: + return False + self.custom_peers.remove(multiaddr) + logger.info("relay_peer_removed", multiaddr=multiaddr) + return True + + def register_peer_pubkey(self, peer_id: str, pubkey_bytes: bytes) -> None: + """ + Register a peer's secp256k1 pubkey for Phase 2 onion routing. + Called by the bridge when peers exchange keys during handshake. + """ + self._peer_pubkeys[peer_id] = pubkey_bytes + + def update_nostr_relays(self, relay_urls: List[str]) -> None: + """ + Update Nostr relay URLs (called when EndpointPreferences change). + Takes effect on the next relay start. + """ + if self._nostr: + self._nostr.relay_urls = list(relay_urls) + else: + self._nostr = NostrTransport(list(relay_urls)) + + # ------------------------------------------------------------------------- + # Messaging + # ------------------------------------------------------------------------- + + async def send_message( + self, + recipient_qube_id: str, + encrypted_payload: bytes, + ttl_days: int = 7, + recipient_pubkey_hex: Optional[str] = None, + ) -> bool: + """ + Send an encrypted message to a recipient Qube. + + Transport fallback waterfall: + 1. DHT routing via live p2pd bridge (with Phase 2 onion wrap) + 2. Nostr relay fallback (Phase 3, requires recipient_pubkey_hex) + 3. Store-and-forward queue (held until recipient comes online) + + Args: + recipient_qube_id: 8-char Qube token prefix. + encrypted_payload: Already ECIES-encrypted bytes. + ttl_days: Store-and-forward retention if all transports fail. + recipient_pubkey_hex: secp256k1 pubkey hex for Nostr fallback addressing. + """ + if self._bridge and self.is_running: + try: + # Phase 2: wrap in 2-hop onion if 2+ peers with known pubkeys are available + payload_to_send = encrypted_payload + online_peers_with_keys = [ + (pid, pk) + for pid, pk in self._peer_pubkeys.items() + if self._peer_status.get(pid, {}).get("online") + ] + if len(online_peers_with_keys) >= 2: + r1_id, r1_pk = online_peers_with_keys[0] + r2_id, r2_pk = online_peers_with_keys[1] + r1_addr = next( + (p["multiaddr"] for p in self.get_peers() if p.get("online") and r1_id in p["multiaddr"]), + r1_id, + ) + r2_addr = next( + (p["multiaddr"] for p in self.get_peers() if p.get("online") and r2_id in p["multiaddr"]), + r2_id, + ) + payload_to_send = await self._onion.wrap_message( + encrypted_payload, + r1_pk, + r2_pk, + b"", # dest pubkey unknown at this layer; inner already ECIES-encrypted + f"qube/{recipient_qube_id}/inbox", + r2_addr, + ) + + topic = f"qube/{recipient_qube_id}/inbox" + await self._bridge.publish(topic, payload_to_send) + logger.info("relay_message_sent", recipient=recipient_qube_id, bytes=len(payload_to_send)) + return True + except Exception as exc: + logger.warning("relay_direct_send_failed", recipient=recipient_qube_id, error=str(exc)) + + # Phase 3: Nostr fallback if DHT unreachable and recipient pubkey is known + if ( + self._nostr + and self._nostr._running + and self._nostr_privkey_int + and recipient_pubkey_hex + ): + try: + nostr_ok = await self._nostr.send( + recipient_pubkey_hex, + encrypted_payload, + our_privkey_int=self._nostr_privkey_int, + ) + if nostr_ok: + logger.info("relay_nostr_fallback_sent", recipient=recipient_qube_id) + return True + except Exception as nostr_exc: + logger.warning("relay_nostr_fallback_failed", error=str(nostr_exc)) + + # Fall back to store-and-forward + import time + ttl_ts = int(time.time()) + ttl_days * 86400 + self.store_forward.enqueue(recipient_qube_id, encrypted_payload, ttl_ts) + logger.info("relay_message_queued", recipient=recipient_qube_id) + return True + + async def get_pending_messages(self, qube_id: str) -> List[Dict[str, Any]]: + """Return and drain store-and-forward messages for a Qube.""" + messages = self.store_forward.dequeue(qube_id) + return [{"payload": m["payload"], "queued_at": m["queued_at"]} for m in messages] + + # ------------------------------------------------------------------------- + # Background peer polling + # ------------------------------------------------------------------------- + + async def _poll_peers_loop(self) -> None: + """Poll all known peers for reachability every 30 s.""" + while self.is_running: + await self._check_all_peers() + await asyncio.sleep(30) + + async def _check_all_peers(self) -> None: + """Ping all peers and update status cache.""" + all_addrs: List[str] = [] + for relay in BUILTIN_RELAYS: + all_addrs.extend(relay["multiaddrs"]) + all_addrs.extend(self.custom_peers) + + for addr in all_addrs: + start = time.monotonic() + online = False + latency_ms = None + try: + if self._bridge: + await asyncio.wait_for(self._bridge.ping(addr), timeout=5.0) + latency_ms = int((time.monotonic() - start) * 1000) + online = True + except Exception: + pass + self._peer_status[addr] = { + "online": online, + "latency_ms": latency_ms, + "last_checked": int(time.time()), + } diff --git a/network/store_forward.py b/network/store_forward.py new file mode 100644 index 0000000..645d4fd --- /dev/null +++ b/network/store_forward.py @@ -0,0 +1,127 @@ +""" +Store-and-Forward Queue — Phase 1 + +JSON file-backed message queue that holds encrypted messages for offline +recipients. Messages are purged after delivery or when TTL expires (default 7 days). + +No new dependencies — pure stdlib JSON + file I/O. +""" + +import json +import os +import time +from pathlib import Path +from typing import Any, Dict, List + +from utils.logging import get_logger + +logger = get_logger(__name__) + +_QUEUE_FILE = "relay_queue.json" + + +class StoreForwardQueue: + """ + Encrypted message queue for offline delivery. + + Storage format (relay_queue.json): + { + "recipient_qube_id": [ + {"payload_hex": "...", "queued_at": 1234567890, "ttl": 1234567890}, + ... + ] + } + """ + + def __init__(self, queue_dir: Path, retention_days: int = 7): + self.queue_file = Path(queue_dir) / _QUEUE_FILE + self.retention_seconds = retention_days * 86400 + self._queue: Dict[str, List[Dict[str, Any]]] = {} + self._load() + + # ------------------------------------------------------------------------- + # Public API + # ------------------------------------------------------------------------- + + def enqueue(self, recipient_id: str, encrypted_payload: bytes, ttl_timestamp: int) -> None: + """Hold an encrypted message until the recipient connects.""" + if recipient_id not in self._queue: + self._queue[recipient_id] = [] + + self._queue[recipient_id].append({ + "payload_hex": encrypted_payload.hex(), + "queued_at": int(time.time()), + "ttl": ttl_timestamp, + }) + self._save() + logger.info("sfq_enqueued", recipient=recipient_id, queue_depth=len(self._queue[recipient_id])) + + def dequeue(self, recipient_id: str) -> List[Dict[str, Any]]: + """ + Return all pending messages for a recipient and remove them from the queue. + Returns list of dicts with keys: payload (bytes), queued_at (int). + """ + if recipient_id not in self._queue: + return [] + + messages = self._queue.pop(recipient_id) + self._save() + + now = int(time.time()) + result = [] + for msg in messages: + if msg["ttl"] >= now: + result.append({ + "payload": bytes.fromhex(msg["payload_hex"]), + "queued_at": msg["queued_at"], + }) + logger.info("sfq_dequeued", recipient=recipient_id, count=len(result)) + return result + + def purge_expired(self) -> int: + """Delete all messages whose TTL has passed. Returns number of messages purged.""" + now = int(time.time()) + total_purged = 0 + + for recipient_id in list(self._queue.keys()): + before = len(self._queue[recipient_id]) + self._queue[recipient_id] = [m for m in self._queue[recipient_id] if m["ttl"] >= now] + purged = before - len(self._queue[recipient_id]) + total_purged += purged + if not self._queue[recipient_id]: + del self._queue[recipient_id] + + if total_purged: + self._save() + logger.info("sfq_purged", count=total_purged) + + return total_purged + + def queue_depth(self, recipient_id: str) -> int: + """Return number of pending messages for a recipient.""" + return len(self._queue.get(recipient_id, [])) + + def total_queued(self) -> int: + """Return total messages across all recipients.""" + return sum(len(msgs) for msgs in self._queue.values()) + + # ------------------------------------------------------------------------- + # Internal persistence + # ------------------------------------------------------------------------- + + def _load(self) -> None: + if not self.queue_file.exists(): + self._queue = {} + return + try: + with open(self.queue_file, "r", encoding="utf-8") as f: + self._queue = json.load(f) + except (json.JSONDecodeError, OSError): + self._queue = {} + + def _save(self) -> None: + try: + with open(self.queue_file, "w", encoding="utf-8") as f: + json.dump(self._queue, f) + except OSError as exc: + logger.warning("sfq_save_failed", error=str(exc)) diff --git a/network/wifi_direct_transport.py b/network/wifi_direct_transport.py new file mode 100644 index 0000000..d9c9877 --- /dev/null +++ b/network/wifi_direct_transport.py @@ -0,0 +1,136 @@ +""" +WiFi-Direct Transport Driver — Phase 5 (Stub) + +Peer-to-peer WiFi connection without an access point. +Faster than BLE (typically 10–250 Mbps vs BLE's ~1 Mbps). +Shorter range than LoRa (30–200 m vs LoRa's 1–15 km). + +Platform support: + Android: Native WiFi-Direct API via Android Nearby (go-libp2p mobile) + iOS: Multipeer Connectivity Framework (abstracts over BLE + WiFi-Direct) + Windows: Wi-Fi Direct API (Windows.Devices.WiFiDirect namespace) + Linux: wpa_supplicant P2P mode (p2p_find, p2p_connect commands) + macOS: No public WiFi-Direct API (use BLE or AirDrop workaround) + +Python library options: + Linux: python-wpasupplicant or direct wpa_cli subprocess calls + Windows: No mature pure-Python library; use WinRT COM interop via comtypes + +Mobile (Android/iOS): + WiFi-Direct on mobile is handled entirely by the gomobile relay layer + (see mobile/android/relay_node_android.py). This desktop driver is for + Windows and Linux desktop relays only. + +Addressing: + Multiaddr format: /wifi-direct/MAC:AA:BB:CC:DD:EE:FF/p2p/QmPeerID + The MAC address is the WiFi interface MAC of the peer's P2P group owner. + +Integration with relay: + Priority 2 in transport waterfall (after local relay, before internet DHT). + Falls back gracefully if WiFi-Direct is unavailable on the platform. + +TODO Phase 5: + 1. Linux: subprocess wpa_cli wrapper for p2p_find + p2p_connect + 2. Windows: WinRT WiFi-Direct API via ctypes/comtypes + 3. Integrate with RelayNodeManager transport list + 4. Test interoperability between Linux and Android (most common case) +""" + +import asyncio +import struct +from typing import Any, Dict, List, Optional + +from utils.logging import get_logger + +logger = get_logger(__name__) + +CHUNK_SIZE = 60000 # WiFi-Direct can handle much larger packets than BLE +SCAN_TIMEOUT_SECS = 10.0 + +_WIFI_DIRECT_AVAILABLE = False # Will be True once platform driver is implemented + + +class WiFiDirectTransport: + """ + WiFi-Direct transport driver for Qubes relay (desktop: Linux + Windows). + + Implements the 4-function relay transport interface over peer-to-peer WiFi. + Requires platform-specific WiFi P2P support. + + Mobile WiFi-Direct is handled by the Android gomobile layer, not this class. + """ + + def __init__(self) -> None: + if not _WIFI_DIRECT_AVAILABLE: + raise ImportError( + "WiFi-Direct transport is not yet implemented for this platform. " + "Use BLE transport (ble_transport.py) for offline mesh until Phase 5." + ) + self._receive_buffers: Dict[str, bytearray] = {} + self._receive_events: Dict[str, asyncio.Event] = {} + + async def discover_local_peers(self) -> List[str]: + """ + Scan for nearby WiFi-Direct peers advertising the Qubes service. + Returns list of multiaddrs: ["/wifi-direct/MAC:.../p2p/...", ...] + + TODO Phase 5: platform P2P discovery. + """ + logger.info("wifi_direct_scan_start", timeout=SCAN_TIMEOUT_SECS) + return [] + + async def connect(self, peer_multiaddr: str) -> Any: + """ + Initiate a WiFi-Direct P2P connection. + + Args: + peer_multiaddr: "/wifi-direct/MAC:AA:BB:CC:DD:EE:FF/p2p/..." format. + + Returns: + Stream handle (socket or similar) for send/receive. + """ + parts = peer_multiaddr.split("/") + mac = "" + for part in parts: + if part.startswith("MAC:"): + mac = part[4:] + break + if not mac: + raise ValueError(f"Cannot parse WiFi-Direct MAC from multiaddr: {peer_multiaddr}") + + logger.info("wifi_direct_connect", mac=mac) + # TODO Phase 5: establish P2P group, open TCP socket over link-local addr + stream = {"mac": mac, "transport": self} + self._receive_buffers[mac] = bytearray() + self._receive_events[mac] = asyncio.Event() + return stream + + async def send(self, stream: Any, data: bytes) -> None: + """ + Send data over a WiFi-Direct connection. + WiFi-Direct MTU is large (jumbo frames), chunk only if needed. + """ + mac = stream["mac"] + fragments = [data[i:i + CHUNK_SIZE] for i in range(0, len(data), CHUNK_SIZE)] + total = len(fragments) + for seq, fragment in enumerate(fragments): + header = struct.pack(">II", seq, total) + packet = header + fragment + # TODO Phase 5: write to TCP socket over P2P link-local address + logger.debug("wifi_direct_send", mac=mac[:11], seq=seq, total=total) + + async def receive(self, stream: Any) -> bytes: + """Wait for and return the next complete message.""" + mac = stream["mac"] + event = self._receive_events.get(mac) + if event: + await event.wait() + event.clear() + data = bytes(self._receive_buffers.get(mac, bytearray())) + self._receive_buffers[mac] = bytearray() + return data + + +def is_wifi_direct_available() -> bool: + """Return True if WiFi-Direct transport is available on this platform.""" + return _WIFI_DIRECT_AVAILABLE diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..f952e33 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,21 @@ +# Patches + +These files patch the PyInstaller bundle at `D:\Qubes\qubes-backend\_internal\`. + +## torch/ + +Fixes for missing torch submodules in the bundled build (torch 2.10.0+cpu). The PyInstaller +bundle strips `torch.distributed`, `torch.futures`, and `torch.rpc` — but TTS (Kokoro) and +GPU acceleration code paths import them. Without these patches, Qubes errors on: +- `No module named 'torch.distributed'` +- `cannot import name 'Future' from 'torch.futures'` +- `cannot import name 'nn' from partially initialized module 'torch'` + +### How to apply after a Qubes update: +Copy these files into `D:\Qubes\qubes-backend\_internal\torch\`: +``` +patches/torch/distributed/__init__.py → _internal/torch/distributed/__init__.py +patches/torch/futures/__init__.py → _internal/torch/futures/__init__.py +patches/torch/rpc/__init__.py → _internal/torch/rpc/__init__.py +patches/torch/nn/__init__.py → _internal/torch/nn/__init__.py (replaces existing) +``` diff --git a/patches/torch/distributed/__init__.py b/patches/torch/distributed/__init__.py new file mode 100644 index 0000000..2914ac1 --- /dev/null +++ b/patches/torch/distributed/__init__.py @@ -0,0 +1,169 @@ +""" +torch.distributed stub with auto-submodule import hook. + +Intercepts ANY `import torch.distributed.*` and returns a stub module so code +that imports distributed submodules doesn't crash in this single-machine build. +""" +import sys +import types + + +def is_available() -> bool: + return False + + +def is_initialized() -> bool: + return False + + +def is_nccl_available() -> bool: + return False + + +def is_gloo_available() -> bool: + return False + + +def is_mpi_available() -> bool: + return False + + +def init_process_group(*args, **kwargs) -> None: + pass + + +def get_rank(*args, **kwargs) -> int: + return 0 + + +def get_world_size(*args, **kwargs) -> int: + return 1 + + +def barrier(*args, **kwargs) -> None: + pass + + +def all_reduce(*args, **kwargs): + pass + + +def broadcast(*args, **kwargs): + pass + + +def destroy_process_group(*args, **kwargs) -> None: + pass + + +def new_group(*args, **kwargs): + return None + + +class ReduceOp: + SUM = 0 + PRODUCT = 1 + MIN = 2 + MAX = 3 + BAND = 4 + BOR = 5 + BXOR = 6 + + +Backend = None +GroupMember = None +group = None + + +def _make_stub_class(name: str) -> type: + """Create a stub class that can be used as a base class and has is_available().""" + return type(name, (object,), { + '__init__': lambda self, *a, **kw: None, + 'is_available': staticmethod(lambda: False), + 'is_initialized': staticmethod(lambda: False), + }) + + +class _StubModule(types.ModuleType): + """ + A module stub that returns stub classes for any attribute access. + Returning classes (not plain functions) ensures code can: + - Use attributes as base classes: class Foo(Joinable): ... + - Call methods on them: dist.rpc.is_available() + """ + + def __init__(self, name: str) -> None: + super().__init__(name) + self.__package__ = name + self.__path__: list = [] # makes Python treat it as a package + self.__spec__ = None + self._stub_cache: dict = {} + + def __getattr__(self, name: str): + if name.startswith('__'): + raise AttributeError(name) + if name not in self.__dict__.get('_stub_cache', {}): + stub = _make_stub_class(name) + self.__dict__.setdefault('_stub_cache', {})[name] = stub + return self.__dict__['_stub_cache'][name] + + +# Pre-build rpc stub with is_available so `dist.rpc.is_available()` works +rpc = _StubModule('torch.distributed.rpc') +rpc.is_available = lambda: False # type: ignore[attr-defined] +sys.modules['torch.distributed.rpc'] = rpc + +# Pre-build other common submodules +for _name in ('distributed_c10d', 'autograd', 'utils', 'algorithms', + 'fsdp', 'optim', 'elastic', 'checkpoint', 'tensor', '_tensor'): + _mod = _StubModule(f'torch.distributed.{_name}') + sys.modules[f'torch.distributed.{_name}'] = _mod + +# Pre-build torch.distributed.algorithms.join (used by nn/parallel/distributed.py) +_algorithms = sys.modules.get('torch.distributed.algorithms', _StubModule('torch.distributed.algorithms')) +_join_mod = _StubModule('torch.distributed.algorithms.join') + + +class Join(_make_stub_class('Join')): + pass + + +class Joinable(_make_stub_class('Joinable')): + pass + + +class JoinHook(_make_stub_class('JoinHook')): + pass + + +_join_mod.Join = Join # type: ignore[attr-defined] +_join_mod.Joinable = Joinable # type: ignore[attr-defined] +_join_mod.JoinHook = JoinHook # type: ignore[attr-defined] +sys.modules['torch.distributed.algorithms.join'] = _join_mod +sys.modules['torch.distributed.algorithms'] = _algorithms + + +class _DistributedImporter: + """ + Meta path finder that intercepts any `torch.distributed.*` import + and returns a _StubModule, preventing ModuleNotFoundError. + """ + + PREFIX = "torch.distributed." + + def find_module(self, fullname: str, path=None): + if fullname.startswith(self.PREFIX): + return self + return None + + def load_module(self, fullname: str): + if fullname in sys.modules: + return sys.modules[fullname] + mod = _StubModule(fullname) + sys.modules[fullname] = mod + return mod + + +# Install the import hook +if not any(isinstance(f, _DistributedImporter) for f in sys.meta_path): + sys.meta_path.append(_DistributedImporter()) diff --git a/patches/torch/futures/__init__.py b/patches/torch/futures/__init__.py new file mode 100644 index 0000000..021bfc1 --- /dev/null +++ b/patches/torch/futures/__init__.py @@ -0,0 +1,89 @@ +""" +torch.futures stub — provides a minimal Future class so TTS/model code +that does `from torch.futures import Future` doesn't crash. + +This file will be replaced by the real module if the update package contains it. +""" +import threading +from typing import Any, Callable, Generic, List, Optional, TypeVar + +T = TypeVar("T") + + +class Future(Generic[T]): + """Minimal stub for torch.futures.Future.""" + + def __init__(self) -> None: + self._result: Optional[Any] = None + self._exception: Optional[BaseException] = None + self._done = False + self._callbacks: List[Callable] = [] + self._lock = threading.Lock() + self._event = threading.Event() + + def done(self) -> bool: + return self._done + + def result(self) -> T: + self._event.wait() + if self._exception is not None: + raise self._exception + return self._result # type: ignore[return-value] + + def set_result(self, result: T) -> None: + with self._lock: + self._result = result + self._done = True + self._event.set() + for cb in self._callbacks: + try: + cb(self) + except Exception: + pass + + def set_exception(self, exception: BaseException) -> None: + with self._lock: + self._exception = exception + self._done = True + self._event.set() + + def add_done_callback(self, callback: Callable) -> None: + with self._lock: + if self._done: + callback(self) + else: + self._callbacks.append(callback) + + def wait(self) -> "Future[T]": + self._event.wait() + return self + + def then(self, callback: Callable) -> "Future": + fut: Future = Future() + def _cb(f: "Future") -> None: + try: + fut.set_result(callback(f)) + except Exception as e: + fut.set_exception(e) + self.add_done_callback(_cb) + return fut + + +def collect_all(futures: List[Future]) -> Future: + """Wait for all futures and return a future of list of results.""" + result_future: Future = Future() + if not futures: + result_future.set_result([]) + return result_future + results = [None] * len(futures) + remaining = [len(futures)] + lock = threading.Lock() + for i, f in enumerate(futures): + def _cb(fut, idx=i): + with lock: + results[idx] = fut.result() + remaining[0] -= 1 + if remaining[0] == 0: + result_future.set_result(results) + f.add_done_callback(_cb) + return result_future diff --git a/patches/torch/nn/__init__.py b/patches/torch/nn/__init__.py new file mode 100644 index 0000000..e2be695 --- /dev/null +++ b/patches/torch/nn/__init__.py @@ -0,0 +1,66 @@ +# mypy: allow-untyped-defs +from torch.nn.parameter import ( # usort: skip + Buffer as Buffer, + Parameter as Parameter, + UninitializedBuffer as UninitializedBuffer, + UninitializedParameter as UninitializedParameter, +) +# Pre-import functional and init so submodules can do `import torch.nn.functional as F` +# without hitting a circular import during torch.nn initialization +import torch.nn.functional # noqa: E402 +import torch.nn.init # noqa: E402 +from torch.nn.modules import * # usort: skip # noqa: F403 +from torch.nn import ( + attention as attention, + functional as functional, + init as init, + modules as modules, + parallel as parallel, + parameter as parameter, + utils as utils, +) +from torch.nn.parallel import DataParallel as DataParallel + + +def factory_kwargs(kwargs): + r"""Return a canonicalized dict of factory kwargs. + + Given kwargs, returns a canonicalized dict of factory kwargs that can be directly passed + to factory functions like torch.empty, or errors if unrecognized kwargs are present. + + This function makes it simple to write code like this:: + + class MyModule(nn.Module): + def __init__(self, **kwargs): + factory_kwargs = torch.nn.factory_kwargs(kwargs) + self.weight = Parameter(torch.empty(10, **factory_kwargs)) + + Why should you use this function instead of just passing `kwargs` along directly? + + 1. This function does error validation, so if there are unexpected kwargs we will + immediately report an error, instead of deferring it to the factory call + 2. This function supports a special `factory_kwargs` argument, which can be used to + explicitly specify a kwarg to be used for factory functions, in the event one of the + factory kwargs conflicts with an already existing argument in the signature (e.g. + in the signature ``def f(dtype, **kwargs)``, you can specify ``dtype`` for factory + functions, as distinct from the dtype argument, by saying + ``f(dtype1, factory_kwargs={"dtype": dtype2})``) + """ + if kwargs is None: + return {} + simple_keys = {"device", "dtype", "memory_format"} + expected_keys = simple_keys | {"factory_kwargs"} + if not kwargs.keys() <= expected_keys: + raise TypeError(f"unexpected kwargs {kwargs.keys() - expected_keys}") + + # guarantee no input kwargs is untouched + r = dict(kwargs.get("factory_kwargs", {})) + for k in simple_keys: + if k in kwargs: + if k in r: + raise TypeError( + f"{k} specified twice, in **kwargs and in factory_kwargs" + ) + r[k] = kwargs[k] + + return r diff --git a/patches/torch/rpc/__init__.py b/patches/torch/rpc/__init__.py new file mode 100644 index 0000000..9c6a379 --- /dev/null +++ b/patches/torch/rpc/__init__.py @@ -0,0 +1,2 @@ +# Stub: torch.rpc is not available in this bundled build +is_available = lambda: False diff --git a/qubes-gui/.npmrc b/qubes-gui/.npmrc new file mode 100644 index 0000000..c688721 --- /dev/null +++ b/qubes-gui/.npmrc @@ -0,0 +1 @@ +allow-build=esbuild diff --git a/qubes-gui/package.json b/qubes-gui/package.json index f30e56b..72f4604 100644 --- a/qubes-gui/package.json +++ b/qubes-gui/package.json @@ -1,7 +1,7 @@ { "name": "qubes-gui", "private": true, - "version": "0.9.0", + "version": "0.9.1", "type": "module", "scripts": { "dev": "vite", @@ -9,7 +9,10 @@ "preview": "vite preview", "tauri": "tauri", "cleanup": "powershell -ExecutionPolicy Bypass -File cleanup-dev.ps1", - "safe-tauri": "npm run cleanup && tauri dev" + "safe-tauri": "npm run cleanup && tauri dev", + "relay:start": "python ../relay/relay_daemon.py", + "relay:dev": "python ../relay/relay_daemon.py --dev", + "relay:test": "cd .. && pytest relay/ -v --tb=short" }, "dependencies": { "@bitauth/libauth": "^3.1.0-next.8", diff --git a/qubes-gui/pnpm-lock.yaml b/qubes-gui/pnpm-lock.yaml new file mode 100644 index 0000000..ce1fe80 --- /dev/null +++ b/qubes-gui/pnpm-lock.yaml @@ -0,0 +1,4041 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@bitauth/libauth': + specifier: ^3.1.0-next.8 + version: 3.1.0-next.8 + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.4) + '@noble/curves': + specifier: ^1.9.7 + version: 1.9.7 + '@noble/hashes': + specifier: ^1.8.0 + version: 1.8.0 + '@tauri-apps/api': + specifier: ^2 + version: 2.10.1 + '@tauri-apps/plugin-dialog': + specifier: ^2.4.0 + version: 2.6.0 + '@tauri-apps/plugin-fs': + specifier: ^2.4.2 + version: 2.4.5 + '@tauri-apps/plugin-opener': + specifier: ^2 + version: 2.5.3 + '@tauri-apps/plugin-process': + specifier: ^2.3.0 + version: 2.3.1 + '@tauri-apps/plugin-shell': + specifier: ^2.3.1 + version: 2.3.5 + '@tauri-apps/plugin-updater': + specifier: ^2 + version: 2.10.0 + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 + '@walletconnect/modal': + specifier: ^2.7.0 + version: 2.7.0(@types/react@19.2.14)(react@19.2.4) + '@walletconnect/sign-client': + specifier: ^2.23.7 + version: 2.23.8(typescript@5.9.3) + '@xyflow/react': + specifier: ^12.9.0 + version: 12.10.1(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + chess.js: + specifier: ^1.4.0 + version: 1.4.0 + d3: + specifier: ^7.9.0 + version: 7.9.0 + emoji-picker-react: + specifier: ^4.15.0 + version: 4.18.0(react@19.2.4) + framer-motion: + specifier: ^12.29.2 + version: 12.37.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + qrcode.react: + specifier: ^4.2.0 + version: 4.2.0(react@19.2.4) + react: + specifier: ^19.1.0 + version: 19.2.4 + react-chessboard: + specifier: ^5.8.6 + version: 5.10.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-dom: + specifier: ^19.1.0 + version: 19.2.4(react@19.2.4) + react-easy-crop: + specifier: ^5.5.6 + version: 5.5.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + recharts: + specifier: ^3.3.0 + version: 3.8.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1) + zustand: + specifier: ^5.0.8 + version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.10.1 + '@types/react': + specifier: ^19.1.8 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^4.6.0 + version: 4.7.0(vite@7.3.1(jiti@1.21.7)) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.27(postcss@8.5.8) + postcss: + specifier: ^8.5.6 + version: 8.5.8 + tailwindcss: + specifier: ^3.4.18 + version: 3.4.19 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.1.11 + version: 7.3.1(jiti@1.21.7) + +packages: + + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bitauth/libauth@3.1.0-next.8': + resolution: {integrity: sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lit-labs/ssr-dom-shim@1.5.1': + resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} + + '@lit/reactive-element@1.6.3': + resolution: {integrity: sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==} + + '@motionone/animation@10.18.0': + resolution: {integrity: sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==} + + '@motionone/dom@10.18.0': + resolution: {integrity: sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==} + + '@motionone/easing@10.18.0': + resolution: {integrity: sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==} + + '@motionone/generators@10.18.0': + resolution: {integrity: sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==} + + '@motionone/svelte@10.16.4': + resolution: {integrity: sha512-zRVqk20lD1xqe+yEDZhMYgftsuHc25+9JSo+r0a0OWUJFocjSV9D/+UGhX4xgJsuwB9acPzXLr20w40VnY2PQA==} + + '@motionone/types@10.17.1': + resolution: {integrity: sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==} + + '@motionone/utils@10.18.0': + resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==} + + '@motionone/vue@10.16.4': + resolution: {integrity: sha512-z10PF9JV6SbjFq+/rYabM+8CVlMokgl8RFGvieSGNTmrkQanfHn+15XBrhG3BgUfvmTeSeyShfOHpG0i9zEdcg==} + deprecated: Motion One for Vue is deprecated. Use Oku Motion instead https://oku-ui.com/motion + + '@msgpack/msgpack@3.1.3': + resolution: {integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==} + engines: {node: '>= 18'} + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.8.0': + resolution: {integrity: sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.7.0': + resolution: {integrity: sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@tauri-apps/api@2.10.1': + resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==} + + '@tauri-apps/cli-darwin-arm64@2.10.1': + resolution: {integrity: sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.10.1': + resolution: {integrity: sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.10.1': + resolution: {integrity: sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.10.1': + resolution: {integrity: sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.10.1': + resolution: {integrity: sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': + resolution: {integrity: sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.10.1': + resolution: {integrity: sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.10.1': + resolution: {integrity: sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.10.1': + resolution: {integrity: sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.10.1': + resolution: {integrity: sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.10.1': + resolution: {integrity: sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.10.1': + resolution: {integrity: sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-dialog@2.6.0': + resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} + + '@tauri-apps/plugin-fs@2.4.5': + resolution: {integrity: sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==} + + '@tauri-apps/plugin-opener@2.5.3': + resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==} + + '@tauri-apps/plugin-process@2.3.1': + resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} + + '@tauri-apps/plugin-shell@2.3.5': + resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==} + + '@tauri-apps/plugin-updater@2.10.0': + resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@walletconnect/core@2.23.8': + resolution: {integrity: sha512-559+fA6Hh9CkEIOtrWKdDWoa3HL47glDF7D75LbqQzv4v325KXq24KEsjzDPBYr7pI49gQo7P2HpPnY1ax+8Aw==} + engines: {node: '>=18.20.8'} + + '@walletconnect/environment@1.0.1': + resolution: {integrity: sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==} + + '@walletconnect/events@1.0.1': + resolution: {integrity: sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==} + + '@walletconnect/heartbeat@1.2.2': + resolution: {integrity: sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==} + + '@walletconnect/jsonrpc-provider@1.0.14': + resolution: {integrity: sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==} + + '@walletconnect/jsonrpc-types@1.0.4': + resolution: {integrity: sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==} + + '@walletconnect/jsonrpc-utils@1.0.8': + resolution: {integrity: sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==} + + '@walletconnect/jsonrpc-ws-connection@1.0.16': + resolution: {integrity: sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==} + + '@walletconnect/keyvaluestorage@1.1.1': + resolution: {integrity: sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==} + peerDependencies: + '@react-native-async-storage/async-storage': 1.x + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@walletconnect/logger@3.0.2': + resolution: {integrity: sha512-7wR3wAwJTOmX4gbcUZcFMov8fjftY05+5cO/d4cpDD8wDzJ+cIlKdYOXaXfxHLSYeDazMXIsxMYjHYVDfkx+nA==} + + '@walletconnect/modal-core@2.7.0': + resolution: {integrity: sha512-oyMIfdlNdpyKF2kTJowTixZSo0PGlCJRdssUN/EZdA6H6v03hZnf09JnwpljZNfir2M65Dvjm/15nGrDQnlxSA==} + + '@walletconnect/modal-ui@2.7.0': + resolution: {integrity: sha512-gERYvU7D7K1ANCN/8vUgsE0d2hnRemfAFZ2novm9aZBg7TEd/4EgB+AqbJ+1dc7GhOL6dazckVq78TgccHb7mQ==} + + '@walletconnect/modal@2.7.0': + resolution: {integrity: sha512-RQVt58oJ+rwqnPcIvRFeMGKuXb9qkgSmwz4noF8JZGUym3gUAzVs+uW2NQ1Owm9XOJAV+sANrtJ+VoVq1ftElw==} + deprecated: Please follow the migration guide on https://docs.reown.com/appkit/upgrade/wcm + + '@walletconnect/relay-api@1.0.11': + resolution: {integrity: sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==} + + '@walletconnect/relay-auth@1.1.0': + resolution: {integrity: sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==} + + '@walletconnect/safe-json@1.0.2': + resolution: {integrity: sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==} + + '@walletconnect/sign-client@2.23.8': + resolution: {integrity: sha512-7DtFDQZwOK4E9q+TKWL819d01dpNHA3jMcntSsQqSLNU34orbkDB/BJzW4nyWZ6H9DuGHRvibJA9wvfXjOCWBw==} + + '@walletconnect/time@1.0.2': + resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} + + '@walletconnect/types@2.23.8': + resolution: {integrity: sha512-OI/0Z7/8r11EDU9bBPy5nixYgsk6SrTcOvWe9r7Nf2WvkMcPLgV7aS8rb6+nInRmDPfXuyTgzdAox0rtmfJMzg==} + + '@walletconnect/utils@2.23.8': + resolution: {integrity: sha512-vJrRrZFZANWmnEEnWnfVSnpQ+jdjqBb5fqSgp0VGeRX3pNr2KAHJ0TwNnEN+fbhR76JxuFrpcY7HJUT7DHDJ7w==} + + '@walletconnect/window-getters@1.0.1': + resolution: {integrity: sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==} + + '@walletconnect/window-metadata@1.0.1': + resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + + '@xyflow/react@12.10.1': + resolution: {integrity: sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.75': + resolution: {integrity: sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==} + + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + autoprefixer@10.4.27: + resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.10.8: + resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + blakejs@1.2.1: + resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001779: + resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==} + + chess.js@1.4.0: + resolution: {integrity: sha512-BBJgrrtKQOzFLonR0l+k64A98NLemPwNsCskwb+29bRwobUa4iTm51E1kwGPbWXAcfdDa18nad6vpPPKPWarqw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-browser@5.3.0: + resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + electron-to-chromium@1.5.313: + resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} + + emoji-picker-react@4.18.0: + resolution: {integrity: sha512-vLTrLfApXAIciguGE57pXPWs9lPLBspbEpPMiUq03TIli2dHZBiB+aZ0R9/Wat0xmTfcd4AuEzQgSYxEZ8C88Q==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + + es-toolkit@1.44.0: + resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} + + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + flairup@1.0.0: + resolution: {integrity: sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + framer-motion@12.37.0: + resolution: {integrity: sha512-j/QUcZS9Nw3NzZWoAbkzr3ETRFHyVeQMlGOUYUmG15U+uiyn9DqIktYruVPDcqY8I35qYR70JaZBvFmS6p+Pdg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + h3@1.15.6: + resolution: {integrity: sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hey-listen@1.0.8: + resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + idb-keyval@6.2.2: + resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} + + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyvaluestorage-interface@1.0.0: + resolution: {integrity: sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lit-element@3.3.3: + resolution: {integrity: sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==} + + lit-html@2.8.0: + resolution: {integrity: sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==} + + lit@2.8.0: + resolution: {integrity: sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + motion-dom@12.37.0: + resolution: {integrity: sha512-LnppZuwX1jQizRWTl9LBLMN3RbAEmdQkX/2Af0UW70NCqYJI/7GfI83vQP9Ucel/Avc0Tf2ZWy8FHawuc0O6Vg==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@10.16.2: + resolution: {integrity: sha512-p+PurYqfUdcJZvtnmAqu5fJgV2kR0uLFQuBKtLeFVTrYEVllI99tiOTSefVNYuip9ELTEkepIIDftNdze76NAQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-wheel@1.0.1: + resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + ox@0.9.3: + resolution: {integrity: sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.0.0: + resolution: {integrity: sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==} + hasBin: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + proxy-compare@2.5.1: + resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==} + + qrcode.react@4.2.0: + resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + qrcode@1.5.3: + resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + engines: {node: '>=10.13.0'} + hasBin: true + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + react-chessboard@5.10.0: + resolution: {integrity: sha512-Y3PgaCVhnDG3IaQfu86OzTSEIEAUtuU5XwmHWnx3tcFOX7lSoAq81ZFX3MBj6y5a6FzDMTczMVmkkrV2CzTrIw==} + engines: {node: '>=20.11.0', pnpm: '>=9.4.0'} + peerDependencies: + react: ^19.0.0 + react-dom: ^19.0.0 + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-easy-crop@5.5.6: + resolution: {integrity: sha512-Jw3/ozs8uXj3NpL511Suc4AHY+mLRO23rUgipXvNYKqezcFSYHxe4QXibBymkOoY6oOtLVMPO2HNPRHYvMPyTw==} + peerDependencies: + react: '>=16.4.0' + react-dom: '>=16.4.0' + + react-is@19.2.4: + resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + recharts@3.8.0: + resolution: {integrity: sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + slow-redact@0.3.2: + resolution: {integrity: sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + uint8arrays@3.1.1: + resolution: {integrity: sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + unstorage@1.17.4: + resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + valtio@1.11.2: + resolution: {integrity: sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=16.8' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@adraffy/ens-normalize@1.11.1': {} + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bitauth/libauth@3.1.0-next.8': {} + + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': + dependencies: + react: 19.2.4 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.4)': + dependencies: + react: 19.2.4 + tslib: 2.8.1 + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lit-labs/ssr-dom-shim@1.5.1': {} + + '@lit/reactive-element@1.6.3': + dependencies: + '@lit-labs/ssr-dom-shim': 1.5.1 + + '@motionone/animation@10.18.0': + dependencies: + '@motionone/easing': 10.18.0 + '@motionone/types': 10.17.1 + '@motionone/utils': 10.18.0 + tslib: 2.8.1 + + '@motionone/dom@10.18.0': + dependencies: + '@motionone/animation': 10.18.0 + '@motionone/generators': 10.18.0 + '@motionone/types': 10.17.1 + '@motionone/utils': 10.18.0 + hey-listen: 1.0.8 + tslib: 2.8.1 + + '@motionone/easing@10.18.0': + dependencies: + '@motionone/utils': 10.18.0 + tslib: 2.8.1 + + '@motionone/generators@10.18.0': + dependencies: + '@motionone/types': 10.17.1 + '@motionone/utils': 10.18.0 + tslib: 2.8.1 + + '@motionone/svelte@10.16.4': + dependencies: + '@motionone/dom': 10.18.0 + tslib: 2.8.1 + + '@motionone/types@10.17.1': {} + + '@motionone/utils@10.18.0': + dependencies: + '@motionone/types': 10.17.1 + hey-listen: 1.0.8 + tslib: 2.8.1 + + '@motionone/vue@10.16.4': + dependencies: + '@motionone/dom': 10.18.0 + tslib: 2.8.1 + + '@msgpack/msgpack@3.1.3': {} + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.8.0': + dependencies: + '@noble/hashes': 1.7.0 + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.7.0': {} + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@tauri-apps/api@2.10.1': {} + + '@tauri-apps/cli-darwin-arm64@2.10.1': + optional: true + + '@tauri-apps/cli-darwin-x64@2.10.1': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.10.1': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.10.1': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.10.1': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.10.1': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.10.1': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.10.1': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.10.1': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.10.1': + optional: true + + '@tauri-apps/cli@2.10.1': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.10.1 + '@tauri-apps/cli-darwin-x64': 2.10.1 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.10.1 + '@tauri-apps/cli-linux-arm64-gnu': 2.10.1 + '@tauri-apps/cli-linux-arm64-musl': 2.10.1 + '@tauri-apps/cli-linux-riscv64-gnu': 2.10.1 + '@tauri-apps/cli-linux-x64-gnu': 2.10.1 + '@tauri-apps/cli-linux-x64-musl': 2.10.1 + '@tauri-apps/cli-win32-arm64-msvc': 2.10.1 + '@tauri-apps/cli-win32-ia32-msvc': 2.10.1 + '@tauri-apps/cli-win32-x64-msvc': 2.10.1 + + '@tauri-apps/plugin-dialog@2.6.0': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-fs@2.4.5': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-opener@2.5.3': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-process@2.3.1': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-shell@2.3.5': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-updater@2.10.0': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/estree@1.0.8': {} + + '@types/geojson@7946.0.16': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@vitejs/plugin-react@4.7.0(vite@7.3.1(jiti@1.21.7))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.3.1(jiti@1.21.7) + transitivePeerDependencies: + - supports-color + + '@walletconnect/core@2.23.8(typescript@5.9.3)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 3.0.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.8 + '@walletconnect/utils': 2.23.8(typescript@5.9.3) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.44.0 + events: 3.3.0 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/environment@1.0.1': + dependencies: + tslib: 1.14.1 + + '@walletconnect/events@1.0.1': + dependencies: + keyvaluestorage-interface: 1.0.0 + tslib: 1.14.1 + + '@walletconnect/heartbeat@1.2.2': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/time': 1.0.2 + events: 3.3.0 + + '@walletconnect/jsonrpc-provider@1.0.14': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + + '@walletconnect/jsonrpc-types@1.0.4': + dependencies: + events: 3.3.0 + keyvaluestorage-interface: 1.0.0 + + '@walletconnect/jsonrpc-utils@1.0.8': + dependencies: + '@walletconnect/environment': 1.0.1 + '@walletconnect/jsonrpc-types': 1.0.4 + tslib: 1.14.1 + + '@walletconnect/jsonrpc-ws-connection@1.0.16': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@walletconnect/keyvaluestorage@1.1.1': + dependencies: + '@walletconnect/safe-json': 1.0.2 + idb-keyval: 6.2.2 + unstorage: 1.17.4(idb-keyval@6.2.2) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/logger@3.0.2': + dependencies: + '@walletconnect/safe-json': 1.0.2 + pino: 10.0.0 + + '@walletconnect/modal-core@2.7.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + valtio: 1.11.2(@types/react@19.2.14)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - react + + '@walletconnect/modal-ui@2.7.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@walletconnect/modal-core': 2.7.0(@types/react@19.2.14)(react@19.2.4) + lit: 2.8.0 + motion: 10.16.2 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@types/react' + - react + + '@walletconnect/modal@2.7.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@walletconnect/modal-core': 2.7.0(@types/react@19.2.14)(react@19.2.4) + '@walletconnect/modal-ui': 2.7.0(@types/react@19.2.14)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - react + + '@walletconnect/relay-api@1.0.11': + dependencies: + '@walletconnect/jsonrpc-types': 1.0.4 + + '@walletconnect/relay-auth@1.1.0': + dependencies: + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + uint8arrays: 3.1.1 + + '@walletconnect/safe-json@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/sign-client@2.23.8(typescript@5.9.3)': + dependencies: + '@walletconnect/core': 2.23.8(typescript@5.9.3) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 3.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.8 + '@walletconnect/utils': 2.23.8(typescript@5.9.3) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/time@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/types@2.23.8': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 3.0.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/utils@2.23.8(typescript@5.9.3)': + dependencies: + '@msgpack/msgpack': 3.1.3 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 3.0.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.8 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + blakejs: 1.2.1 + detect-browser: 5.3.0 + ox: 0.9.3(typescript@5.9.3) + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - typescript + - uploadthing + - zod + + '@walletconnect/window-getters@1.0.1': + dependencies: + tslib: 1.14.1 + + '@walletconnect/window-metadata@1.0.1': + dependencies: + '@walletconnect/window-getters': 1.0.1 + tslib: 1.14.1 + + '@xyflow/react@12.10.1(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@xyflow/system': 0.0.75 + classcat: 5.0.5 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.75': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + + abitype@1.2.3(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + atomic-sleep@1.0.0: {} + + autoprefixer@10.4.27(postcss@8.5.8): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001779 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.8 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.10.8: {} + + binary-extensions@2.3.0: {} + + blakejs@1.2.1: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.8 + caniuse-lite: 1.0.30001779 + electron-to-chromium: 1.5.313 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + camelcase-css@2.0.1: {} + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001779: {} + + chess.js@1.4.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + classcat@5.0.5: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + commander@7.2.0: {} + + convert-source-map@2.0.0: {} + + cookie-es@1.2.2: {} + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decimal.js-light@2.5.1: {} + + defu@6.1.4: {} + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + destr@2.0.5: {} + + detect-browser@5.3.0: {} + + didyoumean@1.2.2: {} + + dijkstrajs@1.0.3: {} + + dlv@1.1.3: {} + + electron-to-chromium@1.5.313: {} + + emoji-picker-react@4.18.0(react@19.2.4): + dependencies: + flairup: 1.0.0 + react: 19.2.4 + + emoji-regex@8.0.0: {} + + encode-utf8@1.0.3: {} + + es-toolkit@1.44.0: {} + + es-toolkit@1.45.1: {} + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escalade@3.2.0: {} + + eventemitter3@5.0.1: {} + + eventemitter3@5.0.4: {} + + events@3.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + flairup@1.0.0: {} + + fraction.js@5.3.4: {} + + framer-motion@12.37.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + motion-dom: 12.37.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + h3@1.15.6: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.3 + uncrypto: 0.1.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hey-listen@1.0.8: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + idb-keyval@6.2.2: {} + + immer@10.2.0: {} + + immer@11.1.4: {} + + internmap@2.0.3: {} + + iron-webcrypto@1.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + keyvaluestorage-interface@1.0.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lit-element@3.3.3: + dependencies: + '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit/reactive-element': 1.6.3 + lit-html: 2.8.0 + + lit-html@2.8.0: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@2.8.0: + dependencies: + '@lit/reactive-element': 1.6.3 + lit-element: 3.3.3 + lit-html: 2.8.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lru-cache@11.2.7: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + motion-dom@12.37.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + motion@10.16.2: + dependencies: + '@motionone/animation': 10.18.0 + '@motionone/dom': 10.18.0 + '@motionone/svelte': 10.16.4 + '@motionone/types': 10.17.1 + '@motionone/utils': 10.18.0 + '@motionone/vue': 10.16.4 + + ms@2.1.3: {} + + multiformats@9.9.0: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.4: {} + + node-releases@2.0.36: {} + + normalize-path@3.0.0: {} + + normalize-wheel@1.0.1: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.3 + + on-exit-leak-free@2.1.2: {} + + ox@0.9.3(typescript@5.9.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + path-exists@4.0.0: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.0.0: + dependencies: + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + slow-redact: 0.3.2 + sonic-boom: 4.2.1 + thread-stream: 3.1.0 + + pirates@4.0.7: {} + + pngjs@5.0.0: {} + + postcss-import@15.1.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.8): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.8 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.8 + + postcss-nested@6.2.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + process-warning@5.0.0: {} + + proxy-compare@2.5.1: {} + + qrcode.react@4.2.0(react@19.2.4): + dependencies: + react: 19.2.4 + + qrcode@1.5.3: + dependencies: + dijkstrajs: 1.0.3 + encode-utf8: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + queue-microtask@1.2.3: {} + + quick-format-unescaped@4.0.4: {} + + radix3@1.1.2: {} + + react-chessboard@5.10.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/modifiers': 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-easy-crop@5.5.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + normalize-wheel: 1.0.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tslib: 2.8.1 + + react-is@19.2.4: {} + + react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + redux: 5.0.1 + + react-refresh@0.17.0: {} + + react@19.2.4: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@5.0.0: {} + + real-require@0.2.0: {} + + recharts@3.8.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.45.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + reselect@5.1.1: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + robust-predicates@3.0.2: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rw@1.3.3: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + set-blocking@2.0.0: {} + + slow-redact@0.3.2: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.8 + postcss-import: 15.1.0(postcss@8.5.8) + postcss-js: 4.1.0(postcss@8.5.8) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8) + postcss-nested: 6.2.0(postcss@8.5.8) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + uint8arrays@3.1.1: + dependencies: + multiformats: 9.9.0 + + uncrypto@0.1.3: {} + + unstorage@1.17.4(idb-keyval@6.2.2): + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.6 + lru-cache: 11.2.7 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.3 + optionalDependencies: + idb-keyval: 6.2.2 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.2.0(react@19.2.4): + dependencies: + react: 19.2.4 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + util-deprecate@1.0.2: {} + + valtio@1.11.2(@types/react@19.2.14)(react@19.2.4): + dependencies: + proxy-compare: 2.5.1 + use-sync-external-store: 1.2.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.4 + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite@7.3.1(jiti@1.21.7): + dependencies: + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + jiti: 1.21.7 + + which-module@2.0.1: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + ws@7.5.10: {} + + y18n@4.0.3: {} + + yallist@3.1.1: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + zustand@4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + immer: 11.1.4 + react: 19.2.4 + + zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + optionalDependencies: + '@types/react': 19.2.14 + immer: 11.1.4 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) diff --git a/qubes-gui/src-tauri/Cargo.lock b/qubes-gui/src-tauri/Cargo.lock index 0e275cb..33cb1c3 100644 --- a/qubes-gui/src-tauri/Cargo.lock +++ b/qubes-gui/src-tauri/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -19,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -52,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -65,27 +56,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "ashpd" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" -dependencies = [ - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "tokio", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus", -] - [[package]] name = "async-broadcast" version = "0.7.2" @@ -112,9 +82,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -144,9 +114,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", @@ -179,7 +149,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -214,7 +184,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -252,21 +222,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - [[package]] name = "base64" version = "0.21.7" @@ -279,6 +234,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -287,11 +257,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -303,22 +273,13 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2", ] [[package]] @@ -357,15 +318,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -375,9 +336,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -388,7 +349,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "cairo-sys-rs", "glib", "libc", @@ -409,9 +370,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -436,7 +397,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -446,14 +407,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.7", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.40" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -488,21 +449,15 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -575,11 +530,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -592,7 +547,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "core-foundation 0.10.1", "libc", ] @@ -632,9 +587,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -657,6 +612,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -664,7 +632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -674,14 +642,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -689,34 +657,33 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -730,7 +697,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -743,7 +710,28 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", ] [[package]] @@ -777,22 +765,16 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", + "bitflags 2.11.0", + "block2", "libc", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -803,23 +785,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading", + "syn 2.0.117", ] [[package]] name = "dlopen2" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -829,20 +802,29 @@ dependencies = [ [[package]] name = "dlopen2_derive" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] -name = "downcast-rs" -version = "1.2.1" +name = "dom_query" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +checksum = "4d9c2e7f1d22d0f2ce07626d259b8a55f4a47cb0938d4006dd8ae037f17d585e" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.36.1", + "precomputed-hash", + "selectors 0.35.0", + "tendril", +] [[package]] name = "dpi" @@ -855,9 +837,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -882,14 +864,14 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "embed-resource" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.7", + "toml 0.9.12+spec-1.1.0", "vswhom", "winreg", ] @@ -911,9 +893,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -933,7 +915,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -944,9 +926,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -1011,27 +993,26 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1043,6 +1024,18 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1070,7 +1063,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1106,24 +1099,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1132,9 +1125,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1151,32 +1144,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1185,7 +1178,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1320,36 +1312,39 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", - "wasm-bindgen", + "r-efi 5.3.0", + "wasip2", ] [[package]] -name = "gimli" -version = "0.32.3" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] [[package]] name = "gio" @@ -1389,7 +1384,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -1417,7 +1412,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1496,14 +1491,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1511,7 +1506,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1526,9 +1521,18 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1562,18 +1566,27 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" +dependencies = [ + "log", + "markup5ever 0.36.1", +] + [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1614,9 +1627,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -1648,7 +1661,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -1669,14 +1681,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1695,9 +1706,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1719,9 +1730,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", "png", @@ -1729,9 +1740,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1742,9 +1753,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1755,11 +1766,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1770,42 +1780,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1813,6 +1819,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1853,12 +1865,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -1872,28 +1884,17 @@ dependencies = [ "cfb", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1920,9 +1921,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "javascriptcore-rs" @@ -1971,9 +1972,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2007,7 +2008,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "serde", "unicode-segmentation", ] @@ -2018,17 +2019,17 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.11.4", - "selectors", + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.13.0", + "selectors 0.24.0", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libappindicator" @@ -2056,9 +2057,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.176" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -2072,26 +2073,27 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "libc", - "redox_syscall", + "plain", + "redox_syscall 0.7.3", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -2104,15 +2106,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "lru-slab" -version = "0.1.2" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mac" @@ -2129,9 +2125,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril", +] + +[[package]] +name = "markup5ever" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" +dependencies = [ + "log", "tendril", + "web_atoms", ] [[package]] @@ -2142,7 +2149,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2153,9 +2160,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -2174,9 +2181,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minisign-verify" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" [[package]] name = "miniz_oxide" @@ -2190,13 +2197,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2209,22 +2216,22 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -2243,7 +2250,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys", @@ -2273,19 +2280,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -2294,9 +2288,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -2309,9 +2303,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2319,83 +2313,37 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - [[package]] name = "objc2" -version = "0.5.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ - "objc-sys", "objc2-encode", + "objc2-exception-helper", ] [[package]] -name = "objc2" -version = "0.6.3" +name = "objc2-app-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", - "libc", - "objc2 0.6.3", - "objc2-cloud-kit", - "objc2-core-data", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2-foundation", ] [[package]] @@ -2404,9 +2352,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -2415,45 +2363,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", - "objc2-core-graphics", "objc2-io-surface", ] @@ -2472,28 +2385,16 @@ dependencies = [ "cc", ] -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "libc", - "objc2 0.5.2", -] - [[package]] name = "objc2-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", + "bitflags 2.11.0", + "block2", "libc", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] @@ -2503,56 +2404,21 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", ] -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2 0.6.3", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-osa-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-foundation", ] [[package]] @@ -2561,20 +2427,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-security" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -2583,10 +2439,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", ] [[package]] @@ -2595,36 +2451,25 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", - "objc2 0.6.3", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", - "objc2-javascript-core", - "objc2-security", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", + "objc2-foundation", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.3.2" +version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ "dunce", "is-wsl", @@ -2634,11 +2479,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.74" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -2655,20 +2500,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -2708,12 +2553,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2765,7 +2610,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2812,6 +2657,17 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -2832,6 +2688,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -2862,6 +2728,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.10.0" @@ -2886,7 +2762,20 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2913,14 +2802,23 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.1", + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2930,9 +2828,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -2945,6 +2843,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -2952,8 +2856,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.11.4", - "quick-xml 0.38.3", + "indexmap 2.13.0", + "quick-xml", "serde", "time", ] @@ -2987,9 +2891,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -3015,6 +2919,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3037,11 +2951,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.6", + "toml_edit 0.25.4+spec-1.1.0", ] [[package]] @@ -3076,22 +2990,22 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "qubes-gui" -version = "0.8.1" +version = "0.9.1" dependencies = [ "base64 0.22.1", "chrono", "flate2", "futures-util", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "sha2", @@ -3109,82 +3023,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.38.3" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3195,6 +3045,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -3220,16 +3076,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - [[package]] name = "rand_chacha" version = "0.2.2" @@ -3250,16 +3096,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - [[package]] name = "rand_core" version = "0.5.1" @@ -3275,16 +3111,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", + "getrandom 0.2.17", ] [[package]] @@ -3317,7 +3144,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -3326,9 +3162,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3348,14 +3184,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3365,9 +3201,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3376,15 +3212,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -3405,8 +3241,6 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", "rustls-pki-types", "serde", "serde_json", @@ -3414,6 +3248,44 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", "tokio-rustls", "tokio-util", "tower", @@ -3422,34 +3294,32 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", - "webpki-roots", ] [[package]] name = "rfd" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" dependencies = [ - "ashpd", - "block2 0.6.2", + "block2", "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3460,18 +3330,12 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -3489,11 +3353,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -3502,9 +3366,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -3514,21 +3378,59 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -3543,9 +3445,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3558,9 +3460,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3594,9 +3496,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3613,15 +3515,9 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.117", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -3630,12 +3526,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.9.4", + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3643,9 +3539,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3658,14 +3554,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", - "derive_more", + "cssparser 0.29.6", + "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fdfed56cd634f04fe8b9ddf947ae3dc493483e819593d2ba17df9ad05db8b2" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", "smallvec", ] @@ -3718,7 +3633,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3729,20 +3644,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3753,7 +3668,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3767,9 +3682,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -3788,17 +3703,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3807,14 +3722,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3836,7 +3751,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3849,6 +3764,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3900,18 +3824,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "siphasher" @@ -3921,15 +3846,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3939,34 +3864,34 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types 0.5.0", "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", + "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3997,15 +3922,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" @@ -4020,6 +3939,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.4" @@ -4032,6 +3963,18 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -4068,9 +4011,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4094,16 +4037,16 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4133,35 +4076,33 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.3" +version = "0.34.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" +checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", + "bitflags 2.11.0", + "block2", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", - "dispatch", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "parking_lot", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -4179,7 +4120,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -4201,9 +4142,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.8.5" +version = "2.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" dependencies = [ "anyhow", "bytes", @@ -4211,7 +4152,7 @@ dependencies = [ "dirs", "dunce", "embed_plist", - "getrandom 0.3.3", + "getrandom 0.3.4", "glob", "gtk", "heck 0.5.0", @@ -4222,15 +4163,15 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4241,11 +4182,10 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tray-icon", "url", - "urlpattern", "webkit2gtk", "webview2-com", "window-vibrancy", @@ -4254,9 +4194,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.4.1" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" dependencies = [ "anyhow", "cargo_toml", @@ -4270,15 +4210,15 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.7", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.4.0" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" dependencies = [ "base64 0.22.1", "brotli", @@ -4292,9 +4232,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.106", + "syn 2.0.117", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", "uuid", @@ -4303,23 +4243,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.4.0" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.4.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" dependencies = [ "anyhow", "glob", @@ -4328,15 +4268,15 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.7", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-plugin-dialog" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" dependencies = [ "log", "raw-window-handle", @@ -4346,15 +4286,15 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.4.2" +version = "2.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" dependencies = [ "anyhow", "dunce", @@ -4367,28 +4307,28 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.17", - "toml 0.9.7", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", "url", ] [[package]] name = "tauri-plugin-opener" -version = "2.5.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" +checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" dependencies = [ "dunce", "glob", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "open", "schemars 0.8.22", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "windows", "zbus", @@ -4396,9 +4336,9 @@ dependencies = [ [[package]] name = "tauri-plugin-process" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" dependencies = [ "tauri", "tauri-plugin", @@ -4406,9 +4346,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.3.1" +version = "2.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54777d0c0d8add34eea3ced84378619ef5b97996bd967d3038c668feefd21071" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" dependencies = [ "encoding_rs", "log", @@ -4421,15 +4361,15 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] [[package]] name = "tauri-plugin-updater" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61" dependencies = [ "base64 0.22.1", "dirs", @@ -4441,7 +4381,8 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest", + "reqwest 0.13.2", + "rustls", "semver", "serde", "serde_json", @@ -4449,7 +4390,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "url", @@ -4459,23 +4400,23 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.8.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.3", + "objc2", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webview2-com", @@ -4484,17 +4425,16 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.8.1" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", "http", "jni", "log", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", "once_cell", "percent-encoding", "raw-window-handle", @@ -4511,9 +4451,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.7.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" dependencies = [ "anyhow", "brotli", @@ -4521,7 +4461,7 @@ dependencies = [ "ctor", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http", "infer", "json-patch", @@ -4539,8 +4479,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.17", - "toml 0.9.7", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4549,22 +4489,23 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ + "dunce", "embed-resource", - "toml 0.9.7", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4592,11 +4533,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4607,46 +4548,46 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4654,46 +4595,27 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.47.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4708,9 +4630,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -4718,9 +4640,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4743,17 +4665,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] @@ -4767,9 +4689,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] @@ -4780,7 +4711,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -4791,7 +4722,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -4800,36 +4731,36 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.2", + "indexmap 2.13.0", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -4842,11 +4773,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -4872,9 +4803,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -4883,44 +4814,44 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "tray-icon" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.17", - "windows-sys 0.59.0", + "thiserror 2.0.18", + "windows-sys 0.60.2", ] [[package]] @@ -4943,13 +4874,13 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4995,9 +4926,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -5005,6 +4936,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -5013,14 +4950,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -5049,13 +4987,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -5067,9 +5005,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -5129,28 +5067,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wasip2", + "wit-bindgen", ] [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -5159,27 +5097,14 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5188,9 +5113,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5198,124 +5123,113 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wayland-backend" -version = "0.3.11" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "cc", - "downcast-rs", - "rustix", - "scoped-tls", - "smallvec", - "wayland-sys", + "leb128fmt", + "wasmparser", ] [[package]] -name = "wayland-client" -version = "0.31.11" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ - "bitflags 2.9.4", - "rustix", - "wayland-backend", - "wayland-scanner", + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", ] [[package]] -name = "wayland-protocols" -version = "0.32.9" +name = "wasm-streams" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ - "bitflags 2.9.4", - "wayland-backend", - "wayland-client", - "wayland-scanner", + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "wayland-scanner" -version = "0.31.7" +name = "wasm-streams" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ - "proc-macro2", - "quick-xml 0.37.5", - "quote", + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "wayland-sys" -version = "0.31.7" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "dlib", - "log", - "pkg-config", + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", ] [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "web-time" -version = "1.1.0" +name = "web_atoms" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" dependencies = [ - "js-sys", - "wasm-bindgen", + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", ] [[package]] name = "webkit2gtk" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -5337,9 +5251,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -5356,19 +5270,19 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "1.0.4" +name = "webpki-root-certs" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" dependencies = [ "rustls-pki-types", ] [[package]] name = "webview2-com" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -5380,22 +5294,22 @@ dependencies = [ [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "webview2-com-sys" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-core 0.61.2", ] @@ -5437,10 +5351,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -5513,7 +5427,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5524,7 +5438,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5551,13 +5465,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -5856,9 +5770,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -5875,42 +5789,123 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.53.4" +version = "0.54.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6" +checksum = "a24eda84b5d488f99344e54b807138896cee8df0b2d16c793f1f6b80e6d8df1f" dependencies = [ "base64 0.22.1", - "block2 0.6.2", + "block2", "cookie", "crossbeam-channel", "dirs", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -5919,7 +5914,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webkit2gtk-sys", @@ -5963,11 +5958,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -5975,21 +5969,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure", ] [[package]] name = "zbus" -version = "5.11.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", @@ -6005,15 +5999,16 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix", "serde", "serde_repr", - "tokio", "tracing", "uds_windows", - "windows-sys 0.60.2", - "winnow 0.7.13", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -6021,14 +6016,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.11.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -6036,34 +6031,33 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", - "winnow 0.7.13", + "winnow 0.7.15", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6083,21 +6077,21 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -6106,9 +6100,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -6117,13 +6111,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6134,47 +6128,52 @@ checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "arbitrary", "crc32fast", - "indexmap 2.11.4", + "indexmap 2.13.0", "memchr", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zvariant" -version = "5.7.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", "serde", - "url", - "winnow 0.7.13", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.7.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", - "winnow 0.7.13", + "syn 2.0.117", + "winnow 0.7.15", ] diff --git a/qubes-gui/src-tauri/Cargo.toml b/qubes-gui/src-tauri/Cargo.toml index 1cc6b26..6148c2c 100644 --- a/qubes-gui/src-tauri/Cargo.toml +++ b/qubes-gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qubes-gui" -version = "0.8.1" +version = "0.9.1" description = "A Tauri App" authors = ["you"] edition = "2021" @@ -15,11 +15,11 @@ name = "qubes_gui_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -tauri-build = { version = "2.4", features = [] } +tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2.4", features = ["protocol-asset", "devtools"] } -tauri-plugin-opener = "2.5" +tauri = { version = "2", features = ["protocol-asset", "devtools"] } +tauri-plugin-opener = "2" tauri-plugin-process = "2" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" @@ -27,7 +27,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" chrono = "0.4" base64 = "0.22" -tauri-plugin-shell = "2.3.1" +tauri-plugin-shell = "2" tauri-plugin-updater = "2" reqwest = { version = "0.12", features = ["json", "stream"] } tokio = { version = "1", features = ["process", "sync", "time"] } diff --git a/qubes-gui/src-tauri/src/lib.rs b/qubes-gui/src-tauri/src/lib.rs index 7795f9f..7e77971 100644 --- a/qubes-gui/src-tauri/src/lib.rs +++ b/qubes-gui/src-tauri/src/lib.rs @@ -22,6 +22,7 @@ static RATE_LIMITER: Mutex>> = Mutex::new(None); fn get_rate_limit_ms(command: &str) -> Option { match command { "send_message" => Some(500), + "send_direct_p2p_message" => Some(500), "generate_speech" => Some(100), // Reduced from 1000ms to allow faster TTS prefetch "create_qube" => Some(5000), "anchor_session" => Some(2000), @@ -5236,9 +5237,160 @@ async fn process_p2p_message(app_handle: AppHandle, } +// ============================================================================= +// RELAY NODE COMMANDS — Phase 1 +// ============================================================================= + +/// Start the local P2P relay node +#[tauri::command] +async fn init_relay_node(app_handle: AppHandle, user_id: String, password: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + let mut secrets = HashMap::new(); + secrets.insert("password", password.as_str()); + sidecar_execute_with_retry("init-relay-node", args, secrets, Some(&app_handle), None).await +} + +/// Stop the local P2P relay node +#[tauri::command] +async fn stop_relay_node(app_handle: AppHandle, user_id: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + sidecar_execute_with_retry("stop-relay-node", args, HashMap::new(), Some(&app_handle), None).await +} + +/// Get relay node status (running, peer_id, peer_count, online_peers) +#[tauri::command] +async fn get_relay_status(app_handle: AppHandle, user_id: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + sidecar_execute_with_retry("get-relay-status", args, HashMap::new(), Some(&app_handle), None).await +} + +/// Get list of relay peers with live reachability status +#[tauri::command] +async fn get_relay_peers(app_handle: AppHandle, user_id: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + sidecar_execute_with_retry("get-relay-peers", args, HashMap::new(), Some(&app_handle), None).await +} + +/// Add a custom relay peer +#[tauri::command] +async fn add_relay_peer(app_handle: AppHandle, user_id: String, multiaddr: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id, multiaddr]; + sidecar_execute_with_retry("add-relay-peer", args, HashMap::new(), Some(&app_handle), None).await +} + +/// Remove a custom relay peer +#[tauri::command] +async fn remove_relay_peer(app_handle: AppHandle, user_id: String, multiaddr: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id, multiaddr]; + sidecar_execute_with_retry("remove-relay-peer", args, HashMap::new(), Some(&app_handle), None).await +} + +/// Update relay preferences +#[tauri::command] +async fn update_relay_preferences(app_handle: AppHandle, user_id: String, password: String, prefs_json: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id, prefs_json]; + let mut secrets = HashMap::new(); + secrets.insert("password", password.as_str()); + sidecar_execute_with_retry("update-relay-preferences", args, secrets, Some(&app_handle), None).await +} + +/// Send an encrypted direct P2P message via relay +#[tauri::command] +async fn send_direct_p2p_message( + app_handle: AppHandle, + user_id: String, + qube_id: String, + recipient_qube_id: String, + recipient_pub_key: String, + message: String, + password: String, +) -> Result { + check_rate_limit("send_direct_p2p_message")?; + validate_identifier(&user_id, "user_id")?; + validate_identifier(&qube_id, "qube_id")?; + let args = vec![user_id, qube_id, recipient_qube_id, recipient_pub_key, message]; + let mut secrets = HashMap::new(); + secrets.insert("password", password.as_str()); + sidecar_execute_with_retry("send-direct-p2p-message", args, secrets, Some(&app_handle), None).await +} + +/// Drain store-and-forward queue for a Qube +#[tauri::command] +async fn get_direct_p2p_messages(app_handle: AppHandle, user_id: String, qube_id: String, password: String) -> Result { + validate_identifier(&user_id, "user_id")?; + validate_identifier(&qube_id, "qube_id")?; + let args = vec![user_id, qube_id]; + let mut secrets = HashMap::new(); + secrets.insert("password", password.as_str()); + sidecar_execute_with_retry("get-direct-p2p-messages", args, secrets, Some(&app_handle), None).await +} + +/// Check if a newer relay bundle is available (Phase 5) +#[tauri::command] +async fn check_relay_bundle_update(app_handle: AppHandle, user_id: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + sidecar_execute_with_retry("check-relay-bundle-update", args, HashMap::new(), Some(&app_handle), None).await +} + +/// Download and apply the latest relay bundle (Phase 5) +#[tauri::command] +async fn update_relay_bundle(app_handle: AppHandle, user_id: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + sidecar_execute_with_retry("update-relay-bundle", args, HashMap::new(), Some(&app_handle), None).await +} + +// ============================================================================= +// ENDPOINT PREFERENCES COMMANDS +// ============================================================================= + +/// Get network endpoint preferences +#[tauri::command] +async fn get_endpoint_preferences(app_handle: AppHandle, user_id: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + sidecar_execute_with_retry("get-endpoint-preferences", args, HashMap::new(), Some(&app_handle), None).await +} + +/// Update network endpoint preferences +#[tauri::command] +async fn update_endpoint_preferences(app_handle: AppHandle, user_id: String, password: String, prefs_json: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id, prefs_json]; + let mut secrets = HashMap::new(); + secrets.insert("password", password.as_str()); + sidecar_execute_with_retry("update-endpoint-preferences", args, secrets, Some(&app_handle), None).await +} + +/// Reset endpoint preferences to defaults +#[tauri::command] +async fn reset_endpoint_preferences(app_handle: AppHandle, user_id: String, password: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + let mut secrets = HashMap::new(); + secrets.insert("password", password.as_str()); + sidecar_execute_with_retry("reset-endpoint-preferences", args, secrets, Some(&app_handle), None).await +} + +/// Ping all configured Fulcrum + Nostr endpoints and return connected counts +#[tauri::command] +async fn check_endpoints(app_handle: AppHandle, user_id: String) -> Result { + validate_identifier(&user_id, "user_id")?; + let args = vec![user_id]; + sidecar_execute_with_retry("check-endpoints", args, HashMap::new(), Some(&app_handle), None).await +} + /// Send an introduction request to another Qube #[tauri::command] -async fn send_introduction(app_handle: AppHandle, +async fn send_introduction(app_handle: AppHandle, user_id: String, qube_id: String, to_commitment: String, @@ -5901,6 +6053,98 @@ async fn start_ollama() -> Result { Ok(true) } +/// Sweep all BCH from a single Qube's wallet to a target address (without deleting the Qube) +#[tauri::command] +async fn sweep_qube_wallet(app_handle: AppHandle, user_id: String, qube_id: String, sweep_address: String, password: String) -> Result { + let args = vec![user_id, qube_id, sweep_address, password]; + let secrets = HashMap::new(); + let result = sidecar_execute_with_retry("sweep-qube-wallet", args, secrets, Some(&app_handle), None).await?; + let response: serde_json::Value = serde_json::from_value(result) + .map_err(|e| format!("Failed to parse sweep-qube-wallet response: {}", e))?; + Ok(response) +} + +/// Pull an Ollama model, streaming progress events to the frontend +#[tauri::command] +async fn pull_ollama_model(app_handle: AppHandle, model_name: String) -> Result { + use futures_util::StreamExt; + + let client = reqwest::Client::new(); + let body = serde_json::json!({ "name": model_name, "stream": true }); + + let response = client + .post("http://127.0.0.1:11434/api/pull") + .json(&body) + .send() + .await + .map_err(|e| format!("Failed to connect to Ollama: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Ollama pull failed with status: {}", response.status())); + } + + let mut stream = response.bytes_stream(); + let mut buf = String::new(); + + while let Some(chunk) = stream.next().await { + let bytes = chunk.map_err(|e| format!("Stream error: {}", e))?; + buf.push_str(&String::from_utf8_lossy(&bytes)); + + // Process complete NDJSON lines + while let Some(pos) = buf.find('\n') { + let line = buf[..pos].trim().to_string(); + buf = buf[pos + 1..].to_string(); + + if line.is_empty() { + continue; + } + + if let Ok(json) = serde_json::from_str::(&line) { + let payload = serde_json::json!({ + "model": model_name, + "status": json.get("status").and_then(|v| v.as_str()).unwrap_or(""), + "completed": json.get("completed").and_then(|v| v.as_u64()), + "total": json.get("total").and_then(|v| v.as_u64()), + "digest": json.get("digest").and_then(|v| v.as_str()).unwrap_or(""), + }); + let _ = app_handle.emit("ollama-pull-progress", payload); + } + } + } + + // Emit done event + let _ = app_handle.emit("ollama-pull-progress", serde_json::json!({ + "model": model_name, + "status": "done", + "completed": null, + "total": null, + })); + + Ok(true) +} + +/// Check status of local TTS/embedding models via Python sidecar +#[tauri::command] +async fn check_local_tts_models(app_handle: AppHandle, user_id: String) -> Result { + let args = vec![user_id]; + let secrets = HashMap::new(); + let result = sidecar_execute_with_retry("check-local-tts-models", args, secrets, Some(&app_handle), None).await?; + let response: serde_json::Value = serde_json::from_value(result) + .map_err(|e| format!("Failed to parse check-local-tts-models response: {}", e))?; + Ok(response) +} + +/// Re-download/update local TTS and embedding models via Python sidecar +#[tauri::command] +async fn update_local_tts_models(app_handle: AppHandle, user_id: String) -> Result { + let args = vec![user_id]; + let secrets = HashMap::new(); + let result = sidecar_execute_with_retry("update-local-tts-models", args, secrets, Some(&app_handle), None).await?; + let response: serde_json::Value = serde_json::from_value(result) + .map_err(|e| format!("Failed to parse update-local-tts-models response: {}", e))?; + Ok(response) +} + /// Open external URL in default browser (using safe opener plugin) #[tauri::command] async fn open_external_url(url: String) -> Result { @@ -6102,7 +6346,8 @@ async fn export_account_backup(app_handle: AppHandle, user_id: String, export_path: String, export_password: String, - master_password: String + master_password: String, + wallet_sig: Option, ) -> Result { validate_identifier(&user_id, "user_id")?; @@ -6113,6 +6358,9 @@ async fn export_account_backup(app_handle: AppHandle, secrets.insert("password", master_password.as_str()); secrets.insert("master_password", master_password.as_str()); secrets.insert("export_password", export_password.as_str()); + if let Some(sig) = &wallet_sig { + secrets.insert("wallet_sig", sig.as_str()); + } let result = sidecar_execute_with_retry("export-account-backup", args, secrets, Some(&app_handle), Some(300)).await?; @@ -6129,6 +6377,7 @@ async fn import_account_backup(app_handle: AppHandle, import_password: String, master_password: String, wallet_sig: Option, + wallet_address: Option, ) -> Result { validate_identifier(&user_id, "user_id")?; @@ -6141,6 +6390,8 @@ async fn import_account_backup(app_handle: AppHandle, secrets.insert("import_password", import_password.as_str()); let wallet_sig_str = wallet_sig.as_deref().unwrap_or(""); secrets.insert("wallet_sig", wallet_sig_str); + let wallet_address_str = wallet_address.as_deref().unwrap_or(""); + secrets.insert("wallet_address", wallet_address_str); let result = sidecar_execute_with_retry("import-account-backup", args, secrets, Some(&app_handle), Some(300)).await?; @@ -6179,6 +6430,7 @@ async fn import_account_backup_ipfs(app_handle: AppHandle, import_password: String, master_password: String, wallet_sig: Option, + wallet_address: Option, ) -> Result { validate_identifier(&user_id, "user_id")?; @@ -6191,6 +6443,8 @@ async fn import_account_backup_ipfs(app_handle: AppHandle, secrets.insert("import_password", import_password.as_str()); let wallet_sig_str = wallet_sig.as_deref().unwrap_or(""); secrets.insert("wallet_sig", wallet_sig_str); + let wallet_address_str = wallet_address.as_deref().unwrap_or(""); + secrets.insert("wallet_address", wallet_address_str); let result = sidecar_execute_with_retry("import-account-backup-ipfs", args, secrets, Some(&app_handle), Some(300)).await?; @@ -7814,6 +8068,23 @@ pub fn run() { continue_p2p_conversation, inject_p2p_block, send_p2p_user_message, + // Relay Node (Phase 1) + init_relay_node, + stop_relay_node, + get_relay_status, + get_relay_peers, + add_relay_peer, + remove_relay_peer, + update_relay_preferences, + send_direct_p2p_message, + get_direct_p2p_messages, + check_relay_bundle_update, + update_relay_bundle, + // Endpoint Preferences + get_endpoint_preferences, + update_endpoint_preferences, + reset_endpoint_preferences, + check_endpoints, // Setup Wizard get_bundle_dir, check_first_run, @@ -7824,6 +8095,10 @@ pub fn run() { check_ollama_status, get_backend_diagnostics, start_ollama, + sweep_qube_wallet, + pull_ollama_model, + check_local_tts_models, + update_local_tts_models, open_external_url, // Chain Sync (NFT-Bundled Storage) sync_to_chain, diff --git a/qubes-gui/src-tauri/tauri.conf.json b/qubes-gui/src-tauri/tauri.conf.json index daffcc6..0646bf4 100644 --- a/qubes-gui/src-tauri/tauri.conf.json +++ b/qubes-gui/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Qubes", - "version": "0.9.0", + "version": "0.9.1", "identifier": "cash.qube.app", "plugins": { "updater": { diff --git a/qubes-gui/src/App.tsx b/qubes-gui/src/App.tsx index 82adc0d..774c592 100644 --- a/qubes-gui/src/App.tsx +++ b/qubes-gui/src/App.tsx @@ -548,7 +548,6 @@ function App() { {/* Title Bar */}
QUBES ({qubes.length} loaded) -
👤 diff --git a/qubes-gui/src/components/auth/LoginScreen.tsx b/qubes-gui/src/components/auth/LoginScreen.tsx index f867066..d32e03b 100644 --- a/qubes-gui/src/components/auth/LoginScreen.tsx +++ b/qubes-gui/src/components/auth/LoginScreen.tsx @@ -30,7 +30,10 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou // Restore from IPFS state const [showIpfsRestoreModal, setShowIpfsRestoreModal] = useState(false); - const [ipfsInputMode, setIpfsInputMode] = useState<'jwt' | 'cid'>('jwt'); + const [ipfsInputMode, setIpfsInputMode] = useState<'jwt' | 'cid' | 'scan'>('jwt'); + const [loginScanQubes, setLoginScanQubes] = useState>([]); + const [isLoginScanning, setIsLoginScanning] = useState(false); + const [loginScanProgress, setLoginScanProgress] = useState<{ current: number; total: number; step: 'downloading' | 'uploading' | 'done' } | null>(null); const [ipfsPinataJwt, setIpfsPinataJwt] = useState(''); const [ipfsDirectCid, setIpfsDirectCid] = useState(''); const [ipfsBackupList, setIpfsBackupList] = useState>([]); @@ -49,7 +52,7 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou const [walletSig, setWalletSig] = useState(''); const [isConnectingWallet, setIsConnectingWallet] = useState(false); const [wcError, setWcError] = useState(null); - + const [wcCopied, setWcCopied] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!username.trim() || !password.trim()) return; @@ -130,6 +133,7 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou importPassword: restorePassword, masterPassword: restoreMasterPassword, walletSig: walletSig, + walletAddress: wcAddress || null, }); if (result.success) { @@ -158,6 +162,66 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou } }; + const handleLoginScanWallet = async () => { + if (!wcAddress) return; + setIsLoginScanning(true); + setLoginScanQubes([]); + try { + const result = await invoke<{ success: boolean; qubes?: Array<{ qube_id: string; qube_name: string; category_id: string; ipfs_cid: string; chain_length: number; already_imported?: boolean }>; error?: string }>( + 'scan_wallet', { userId: '_restore', walletAddress: wcAddress } + ); + if (result.success && result.qubes) { + setLoginScanQubes(result.qubes); + if (result.qubes.length === 0) setIpfsListError('No Qubes found for this wallet address.'); + } else { + setIpfsListError(result.error || 'Scan failed'); + } + } catch (err) { + setIpfsListError(`${err}`); + } finally { + setIsLoginScanning(false); + } + }; + + const handleLoginScanRestore = async () => { + const importable = loginScanQubes.filter(q => !q.already_imported); + if (!importable.length || !ipfsRestoreMasterPassword) return; + if (ipfsRestoreMasterPassword !== ipfsRestoreMasterPasswordConfirm) { setIpfsRestoreError('Master passwords do not match.'); return; } + if (ipfsRestoreMasterPassword.length < 8) { setIpfsRestoreError('Master password must be at least 8 characters.'); return; } + setIsIpfsRestoring(true); + setIpfsRestoreError(null); + setLoginScanProgress(null); + let imported = 0; + try { + for (let i = 0; i < importable.length; i++) { + const q = importable[i]; + setLoginScanProgress({ current: i + 1, total: importable.length, step: 'downloading' }); + try { + const result = await invoke<{ success: boolean; qube_id?: string; error?: string }>('import_from_wallet', { + userId: '_restore', walletWif: '', categoryId: q.category_id, password: ipfsRestoreMasterPassword, + }); + if (result.success) imported++; + } catch (err) { + console.error(`Failed to import ${q.qube_name}:`, err); + } + } + if (imported > 0) { + setLoginScanProgress({ current: importable.length, total: importable.length, step: 'done' }); + await new Promise(r => setTimeout(r, 1200)); + alert(`${imported} Qube(s) imported from wallet.\n\nPlease sign in with your master password.`); + setShowIpfsRestoreModal(false); + setUsername('_restore'); + setPassword(ipfsRestoreMasterPassword); + } else { + setIpfsRestoreError('No new Qubes were imported.'); + } + } catch (err) { + setIpfsRestoreError(`${err}`); + } finally { + setIsIpfsRestoring(false); + } + }; + const handleListBackups = async () => { if (!ipfsPinataJwt.trim()) return; setIsListingBackups(true); @@ -216,6 +280,7 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou importPassword: ipfsRestorePassword, masterPassword: ipfsRestoreMasterPassword, walletSig: walletSig, + walletAddress: wcAddress || null, }); if (result.success) { @@ -259,21 +324,29 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou <>

Connect your BCH wallet (Cashonize / Zapit) to authenticate.{' '} - Required for wallet-bound backups. + Required for all restores.

- + {wcUri && (

Scan with Cashonize or Zapit:

+
)} {wcError &&

{wcError}

} @@ -371,7 +444,7 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou onClick={() => setShowRestoreModal(true)} className="text-accent-secondary hover:text-accent-secondary/80 text-sm font-medium transition-colors" > - Restore from File + Restore from Local File | +
{/* JWT mode */} @@ -448,6 +527,61 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou
)} + {/* Scan Wallet mode */} + {ipfsInputMode === 'scan' && ( +
+ {!walletSig ? ( +

Connect your wallet below first, then scan.

+ ) : ( + <> + {loginScanQubes.length === 0 && !isLoginScanning && ( + + )} + {isLoginScanning &&

Scanning wallet for Qubes...

} + {loginScanQubes.length > 0 && ( +
+

Found {loginScanQubes.length} Qube(s)

+ {loginScanQubes.map(q => ( +
+ {q.qube_name} + {q.already_imported && (already imported)} +
+ ))} +
+ )} + {loginScanProgress && ( +
+
+
+
+ {[ + { key: 'downloading', icon: '⬇️', label: `Downloading (${loginScanProgress.current}/${loginScanProgress.total})` }, + { key: 'done', icon: '✅', label: 'Done — sign in with your master password' }, + ].map(({ key, icon, label }) => { + const steps = ['downloading', 'done']; + const stepIdx = steps.indexOf(loginScanProgress.step); + const thisIdx = steps.indexOf(key); + return ( +
+ {thisIdx < stepIdx ? '✓' : thisIdx === stepIdx ? icon : '○'} + {label} + {thisIdx === stepIdx && key !== 'done' && ...} +
+ ); + })} +
+ )} + + )} +
+ )} + {ipfsListError && (

{ipfsListError}

@@ -491,8 +625,8 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou {/* WalletConnect — always visible once modal is open */} {walletConnectBlock} - {/* Passwords — shown when CID is known */} - {(ipfsInputMode === 'cid' ? ipfsDirectCid.trim() : ipfsSelectedCid) && ( + {/* Passwords — shown when CID is known or in scan mode */} + {(ipfsInputMode === 'scan' ? loginScanQubes.length > 0 : (ipfsInputMode === 'cid' ? ipfsDirectCid.trim() : ipfsSelectedCid)) && ( <>
@@ -564,14 +698,17 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou > Cancel - {(ipfsInputMode === 'cid' ? ipfsDirectCid.trim() : ipfsSelectedCid) && ( + {(ipfsInputMode === 'scan' ? loginScanQubes.filter(q => !q.already_imported).length > 0 : (ipfsInputMode === 'cid' ? ipfsDirectCid.trim() : ipfsSelectedCid)) && ( - {isIpfsRestoring ? 'Restoring...' : 'Restore Account'} + {isIpfsRestoring ? 'Restoring...' : ipfsInputMode === 'scan' ? 'Import from Wallet' : 'Restore Account'} )}
@@ -579,11 +716,11 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou
)} - {/* Restore from File Modal */} + {/* Restore from Local File Modal */} {showRestoreModal && (
-

Restore from File

+

Restore from Local File

Restore your entire account from a .qube-backup file. This will set up your account and import all Qubes from the backup. @@ -668,7 +805,7 @@ export const LoginScreen: React.FC = ({ onLogin, onCreateAccou {isRestoring ? 'Restoring...' : 'Restore Account'} diff --git a/qubes-gui/src/components/tabs/QubeManagerTab.tsx b/qubes-gui/src/components/tabs/QubeManagerTab.tsx index ae1351c..7fe3f27 100644 --- a/qubes-gui/src/components/tabs/QubeManagerTab.tsx +++ b/qubes-gui/src/components/tabs/QubeManagerTab.tsx @@ -20,6 +20,7 @@ import WalletConnectButton from '../WalletConnectButton'; import { useModels } from '../../hooks/useModels'; import { useChainState } from '../../contexts/ChainStateContext'; import { useVoiceLibrary } from '../../contexts/VoiceLibraryContext'; +import { QRCodeCanvas } from 'qrcode.react'; import { DndContext, closestCenter, @@ -97,7 +98,10 @@ export const QubeManagerTab: React.FC = ({ // Import from wallet modal state (WalletConnect-based, no WIF) const [isScanning, setIsScanning] = useState(false); - const [importProgress, setImportProgress] = useState<{ current: number; total: number; name: string } | null>(null); + const [importProgress, setImportProgress] = useState<{ + current: number; total: number; name: string; + step: 'downloading' | 'uploading' | 'done'; + } | null>(null); // Account backup state const [showBackupModal, setShowBackupModal] = useState(false); @@ -121,7 +125,10 @@ export const QubeManagerTab: React.FC = ({ // Restore mode const [restoreMode, setRestoreMode] = useState<'local' | 'ipfs'>('local'); // IPFS restore state - const [ipfsInputMode, setIpfsInputMode] = useState<'jwt' | 'cid'>('jwt'); + const [ipfsInputMode, setIpfsInputMode] = useState<'jwt' | 'cid' | 'scan'>('jwt'); + const [restoreScanQubes, setRestoreScanQubes] = useState>([]); + const [isRestoreScanning, setIsRestoreScanning] = useState(false); + const [restoreScanProgress, setRestoreScanProgress] = useState<{ current: number; total: number; step: 'downloading' | 'uploading' | 'done' } | null>(null); const [ipfsPinataJwt, setIpfsPinataJwt] = useState(''); const [ipfsDirectCid, setIpfsDirectCid] = useState(''); const [ipfsBackupList, setIpfsBackupList] = useState>([]); @@ -132,9 +139,62 @@ export const QubeManagerTab: React.FC = ({ const [restoreWalletSig, setRestoreWalletSig] = useState(''); const [restoreWcAddress, setRestoreWcAddress] = useState(''); const [isSigningRestoreWc, setIsSigningRestoreWc] = useState(false); + const [restoreWcUri, setRestoreWcUri] = useState(''); + const [restoreWcCopied, setRestoreWcCopied] = useState(false); // Pinata configuration check const [pinataConfigured, setPinataConfigured] = useState(null); + + // Total balance widget state + const { wallets: walletCache, setBalance: setCachedBalanceGlobal } = useWalletCache(); + const [isRefreshingAll, setIsRefreshingAll] = useState(false); + const [showSweepAllModal, setShowSweepAllModal] = useState(false); + const [sweepAllAddress, setSweepAllAddress] = useState(''); + const [isSweepingAll, setIsSweepingAll] = useState(false); + const [sweepAllResult, setSweepAllResult] = useState(null); + + const totalSats = qubes.reduce((sum, q) => sum + (walletCache[q.qube_id]?.balance ?? 0), 0); + const hasAnyBalance = qubes.some((q) => (walletCache[q.qube_id]?.balance ?? 0) > 0); + + const handleRefreshAllBalances = async () => { + if (isRefreshingAll || !userId || !masterPassword) return; + setIsRefreshingAll(true); + try { + await Promise.all( + qubes.filter((q) => q.wallet_address).map(async (q) => { + try { + const result = await invoke<{ success?: boolean; balance_sats?: number }>('get_wallet_info', { + userId, qubeId: q.qube_id, password: masterPassword, + }); + if (result.balance_sats !== undefined) setCachedBalanceGlobal(q.qube_id, result.balance_sats); + } catch { /* ignore per-qube errors */ } + }) + ); + } finally { + setIsRefreshingAll(false); + } + }; + + const handleSweepAll = async () => { + if (!sweepAllAddress || !userId || !masterPassword) return; + setIsSweepingAll(true); + setSweepAllResult(null); + let totalSwept = 0; + const errors: string[] = []; + for (const q of qubes.filter((q) => q.wallet_address && (walletCache[q.qube_id]?.balance ?? 0) > 0)) { + try { + const result = await invoke<{ success: boolean; swept_sats: number; error?: string }>('sweep_qube_wallet', { + userId, qubeId: q.qube_id, sweepAddress: sweepAllAddress, password: masterPassword, + }); + if (result.success) { totalSwept += result.swept_sats; setCachedBalanceGlobal(q.qube_id, 0); } + else errors.push(`${q.name}: ${result.error}`); + } catch (err) { errors.push(`${q.name}: ${String(err)}`); } + } + setSweepAllResult(errors.length === 0 + ? `✓ Swept ${(totalSwept / 1e8).toFixed(8)} BCH from ${qubes.length} qubes` + : `Swept ${(totalSwept / 1e8).toFixed(8)} BCH. Errors: ${errors.join('; ')}`); + setIsSweepingAll(false); + }; const [walletQubes, setWalletQubes] = useState = ({ exportPath: filePath, exportPassword: masterPassword, masterPassword: masterPassword, + walletSig: null, }); if (result.success) { @@ -388,6 +449,7 @@ export const QubeManagerTab: React.FC = ({ setIpfsListError(null); setRestoreWalletSig(''); setRestoreWcAddress(''); + setRestoreWcUri(''); }; // Restore (merge) qubes from a backup file @@ -403,6 +465,7 @@ export const QubeManagerTab: React.FC = ({ importPassword: restorePassword, masterPassword: masterPassword, walletSig: restoreWalletSig || null, + walletAddress: restoreWcAddress || null, }); if (result.success) { @@ -466,6 +529,7 @@ export const QubeManagerTab: React.FC = ({ importPassword: restorePassword, masterPassword, walletSig: restoreWalletSig || null, + walletAddress: restoreWcAddress || null, } ); if (result.success) { @@ -484,13 +548,18 @@ export const QubeManagerTab: React.FC = ({ const handleSignRestoreWc = async () => { setIsSigningRestoreWc(true); + setRestoreWcUri(''); try { - const { getSession, signMessage: wcSignMessage } = await import('../../services/walletConnect'); - const session = await getSession(); + const { connect: wcConnect, getSession, signMessage: wcSignMessage } = await import('../../services/walletConnect'); + let session = await getSession(); + if (!session) { + session = await wcConnect((uri) => setRestoreWcUri(uri)); + setRestoreWcUri(''); + } if (session) { const sig = await wcSignMessage('qubes-backup-key:v1', 'Qubes needs to verify wallet ownership to decrypt your backup', session.topic); setRestoreWalletSig(sig); - setRestoreWcAddress(wallet.address || ''); + setRestoreWcAddress(session.address || wallet.address || ''); } } catch (err) { console.error('WC sign failed:', err); @@ -719,6 +788,76 @@ export const QubeManagerTab: React.FC = ({ } }; + // Scan wallet for Qubes in the restore modal context (uses restoreWcAddress) + const handleRestoreScanWallet = async () => { + if (!restoreWcAddress || !userId) return; + setIsRestoreScanning(true); + setRestoreScanQubes([]); + try { + const result = await invoke<{ success: boolean; qubes?: Array<{ qube_id: string; qube_name: string; category_id: string; ipfs_cid: string; chain_length: number; already_imported?: boolean }>; error?: string }>( + 'scan_wallet', { userId, walletAddress: restoreWcAddress } + ); + if (result.success && result.qubes) { + setRestoreScanQubes(result.qubes); + if (result.qubes.length === 0) alert('No Qubes found for this wallet address.'); + } else { + alert(`Scan failed: ${result.error}`); + } + } catch (err) { + alert(`Scan failed: ${err}`); + } finally { + setIsRestoreScanning(false); + } + }; + + // Restore Qubes found by wallet scan (import_from_wallet + auto-anchor) + const handleScanModeRestore = async () => { + const importable = restoreScanQubes.filter(q => !q.already_imported); + if (!importable.length || !masterPassword) return; + setIsRestoring(true); + setRestoreScanProgress(null); + let imported = 0; + try { + for (let i = 0; i < importable.length; i++) { + const q = importable[i]; + setRestoreScanProgress({ current: i + 1, total: importable.length, step: 'downloading' }); + try { + const result = await invoke<{ success: boolean; qube_id?: string; error?: string }>('import_from_wallet', { + userId, walletWif: '', categoryId: q.category_id, password: masterPassword, + }); + if (result.success) { + imported++; + window.dispatchEvent(new CustomEvent('qube-created', { detail: { qube_id: result.qube_id } })); + } + } catch (err) { + console.error(`Failed to import ${q.qube_name}:`, err); + } + } + if (imported > 0 && pinataConfigured) { + setRestoreScanProgress({ current: importable.length, total: importable.length, step: 'uploading' }); + try { + await invoke('export_account_backup_ipfs', { userId, exportPassword: masterPassword, masterPassword, walletSig: null }); + } catch (e) { + console.warn('IPFS sync after scan restore failed (non-fatal):', e); + } + } + if (imported > 0) { + setRestoreScanProgress({ current: importable.length, total: importable.length, step: 'done' }); + await new Promise(r => setTimeout(r, 1200)); + setShowRestoreModal(false); + resetRestoreState(); + setRestoreScanQubes([]); + setRestoreScanProgress(null); + } else { + alert('No new Qubes were imported.'); + } + } catch (err) { + alert(`Restore failed: ${err}`); + } finally { + setIsRestoring(false); + } + }; + // Bulk import all Qubes from wallet (no WIF — password-based decryption) const handleBulkImport = async () => { const importable = walletQubes.filter(q => !q.already_imported); @@ -731,9 +870,10 @@ export const QubeManagerTab: React.FC = ({ const errors: string[] = []; try { + // Step 1: Download + decrypt + save each Qube for (let i = 0; i < importable.length; i++) { const q = importable[i]; - setImportProgress({ current: i + 1, total: importable.length, name: q.qube_name }); + setImportProgress({ current: i + 1, total: importable.length, name: q.qube_name, step: 'downloading' }); try { const result = await invoke<{ @@ -744,7 +884,7 @@ export const QubeManagerTab: React.FC = ({ error?: string; }>('import_from_wallet', { userId, - walletWif: '', // Empty = password-based decryption + walletWif: '', categoryId: q.category_id, password: masterPassword, }); @@ -762,13 +902,30 @@ export const QubeManagerTab: React.FC = ({ } } - const summary = [`Imported ${imported} of ${importable.length} Qube(s).`]; - if (failed > 0) summary.push(`\n${failed} failed:\n${errors.join('\n')}`); - alert(summary.join('')); + // Step 2: Upload to recipient's own Pinata + if (imported > 0 && pinataConfigured) { + setImportProgress({ current: importable.length, total: importable.length, name: 'All Qubes', step: 'uploading' }); + try { + await invoke('export_account_backup_ipfs', { + userId, + exportPassword: masterPassword, + masterPassword, + walletSig: null, + }); + } catch (e) { + console.warn('IPFS sync after import failed (non-fatal):', e); + } + } if (imported > 0) { + setImportProgress({ current: importable.length, total: importable.length, name: 'All Qubes', step: 'done' }); + await new Promise(r => setTimeout(r, 1200)); setShowImportFromWalletModal(false); resetImportState(); + } else { + const summary = [`Imported ${imported} of ${importable.length} Qube(s).`]; + if (failed > 0) summary.push(`\n${failed} failed:\n${errors.join('\n')}`); + alert(summary.join('')); } } catch (error) { alert(`Import failed: ${error}`); @@ -875,6 +1032,70 @@ export const QubeManagerTab: React.FC = ({ + New Qube + + {/* Total Balance Widget — after New Qube */} +

+ {/* Top row: label + refresh */} +
+ Total Balance + +
+ {/* Balance amount */} +
+ + {isRefreshingAll ? '...' : `${(totalSats / 1e8).toFixed(8)} BCH`} + +
+ {/* Sweep row — below balance */} + {!showSweepAllModal ? ( + + ) : ( +
+ setSweepAllAddress(e.target.value)} + placeholder="bitcoincash:q..." + className="bg-transparent text-[10px] text-text-primary placeholder:text-text-disabled font-mono focus:outline-none flex-1 min-w-0" + autoFocus + /> + + +
+ )} +
+ {sweepAllResult && ( +
+ {sweepAllResult} +
+ )}
@@ -1208,10 +1429,35 @@ export const QubeManagerTab: React.FC = ({ {/* Import progress */} {importProgress && ( -
-

- Importing {importProgress.current} of {importProgress.total}: {importProgress.name}... -

+
+ {/* Progress bar */} +
+
+
+ {/* Steps */} +
+ {[ + { key: 'downloading', icon: '⬇️', label: `Downloading & decrypting from IPFS (${importProgress.current}/${importProgress.total})` }, + { key: 'uploading', icon: '☁️', label: 'Uploading to your Pinata backup...' }, + { key: 'done', icon: '✅', label: 'All done — Qube(s) ready' }, + ].map(({ key, icon, label }) => { + const steps = ['downloading', 'uploading', 'done']; + const stepIdx = steps.indexOf(importProgress.step); + const thisIdx = steps.indexOf(key); + const isDone = thisIdx < stepIdx; + const isActive = thisIdx === stepIdx; + return ( +
+ {isDone ? '✓' : isActive ? icon : '○'} + {label} + {isActive && key !== 'done' && ...} +
+ ); + })} +
)} @@ -1293,9 +1539,9 @@ export const QubeManagerTab: React.FC = ({

Backup

Export your entire account (credentials + all Qubes) to a portable .qube-backup file. - Encrypted with your master password. + Encrypted with your master password. Wallet signature required to restore.

-
+
setShowBackupModal(false)} @@ -1370,7 +1616,9 @@ export const QubeManagerTab: React.FC = ({ multiple: false, filters: [{ name: 'Qube Backup', extensions: ['qube-backup'] }], }); - if (path && typeof path === 'string') setRestoreFilePath(path); + if (path && typeof path === 'string') { + setRestoreFilePath(path); + } }} className="w-full px-3 py-2 bg-glass-bg border border-glass-border rounded-lg text-text-primary text-sm text-left hover:border-accent-primary/50 transition-colors truncate" > @@ -1387,23 +1635,33 @@ export const QubeManagerTab: React.FC = ({ className="w-full px-3 py-2 bg-glass-bg border border-glass-border rounded-lg text-text-primary text-sm" />
- {/* WC 2FA */} + {/* WC 2FA — always required */}
- Optional: Wallet 2FA + Required: Wallet 2FA {restoreWalletSig && ( ✓ {restoreWcAddress.slice(0, 16)}... )}
{!restoreWalletSig ? ( <> -

Required only if your backup was created with wallet authentication.

- {wallet.connected ? ( - - {isSigningRestoreWc ? 'Signing...' : '🔗 Sign with Connected Wallet'} - - ) : ( -

Connect your wallet first using the WalletConnect button.

+

Connect your wallet to prove ownership. Required for all restores.

+ + {isSigningRestoreWc ? 'Connecting...' : '🔗 Connect & Sign Wallet'} + + {restoreWcUri && ( +
+

Scan with Cashonize or Zapit:

+
+ +
+ +
)} ) : ( @@ -1423,7 +1681,7 @@ export const QubeManagerTab: React.FC = ({ {isRestoring ? 'Restoring...' : 'Restore'} @@ -1457,6 +1715,16 @@ export const QubeManagerTab: React.FC = ({ > Direct CID +
{ipfsInputMode === 'jwt' ? ( @@ -1505,7 +1773,7 @@ export const QubeManagerTab: React.FC = ({
)}
- ) : ( + ) : ipfsInputMode === 'cid' ? (
= ({ className="w-full px-3 py-2 bg-glass-bg border border-glass-border rounded-lg text-text-primary text-sm font-mono" />
+ ) : ( + /* Scan Wallet mode */ +
+ {!restoreWalletSig ? ( +

Connect your wallet using the section below, then scan.

+ ) : ( + <> + {restoreScanQubes.length === 0 && !isRestoreScanning && ( + + 🔍 Scan Wallet for Qubes + + )} + {isRestoreScanning && ( +

Scanning wallet for Qubes...

+ )} + {restoreScanQubes.length > 0 && ( +
+

+ Found {restoreScanQubes.length} Qube(s) + {restoreScanQubes.every(q => q.already_imported) && — all already imported} +

+ {restoreScanQubes.map(q => ( +
+ {q.qube_name} + {q.already_imported && (already imported)} + {q.chain_length || 1} block{(q.chain_length || 1) !== 1 ? 's' : ''} +
+ ))} +
+ )} + {restoreScanProgress && ( +
+
+
+
+ {[ + { key: 'downloading', icon: '⬇️', label: `Downloading & decrypting (${restoreScanProgress.current}/${restoreScanProgress.total})` }, + { key: 'uploading', icon: '☁️', label: 'Uploading to your Pinata backup...' }, + { key: 'done', icon: '✅', label: 'All done' }, + ].map(({ key, icon, label }) => { + const steps = ['downloading', 'uploading', 'done']; + const stepIdx = steps.indexOf(restoreScanProgress.step); + const thisIdx = steps.indexOf(key); + return ( +
+ {thisIdx < stepIdx ? '✓' : thisIdx === stepIdx ? icon : '○'} + {label} + {thisIdx === stepIdx && key !== 'done' && ...} +
+ ); + })} +
+ )} + + )} +
)} -
+ {ipfsInputMode !== 'scan' &&
= ({ placeholder="Master password used to encrypt the backup" className="w-full px-3 py-2 bg-glass-bg border border-glass-border rounded-lg text-text-primary text-sm" /> -
+
} {/* WC 2FA */}
- Optional: Wallet 2FA + Required: Wallet 2FA {restoreWalletSig && ( ✓ {restoreWcAddress.slice(0, 16)}... )}
{!restoreWalletSig ? ( <> -

Required only if your backup was created with wallet authentication.

- {wallet.connected ? ( - - {isSigningRestoreWc ? 'Signing...' : '🔗 Sign with Connected Wallet'} - - ) : ( -

Connect your wallet first using the WalletConnect button.

+

Connect your wallet to authenticate backup access. Required for all restores.

+ + {isSigningRestoreWc ? 'Connecting...' : '🔗 Connect & Sign Wallet'} + + {restoreWcUri && ( +
+

Scan with Cashonize or Zapit:

+
+ +
+ +
)} ) : ( @@ -1565,11 +1902,16 @@ export const QubeManagerTab: React.FC = ({ !q.already_imported).length === 0 + : !restorePassword || (ipfsInputMode === 'jwt' ? !ipfsSelectedCid : !ipfsDirectCid.trim())) + } loading={isRestoring} > - {isRestoring ? 'Restoring...' : 'Restore from IPFS'} + {isRestoring ? 'Restoring...' : ipfsInputMode === 'scan' ? 'Import from Wallet' : 'Restore from IPFS'}
@@ -1986,6 +2328,38 @@ const QubeCard: React.FC = ({ qube, allQubes, onEdit, onDelete, o const [walletBalance, setWalletBalance] = useState(cachedWalletData?.balance ?? null); // P2SH wallet const [walletBalanceLoading, setWalletBalanceLoading] = useState(false); const [walletBalanceError, setWalletBalanceError] = useState(cachedWalletData?.error ?? null); + const [showSweepModal, setShowSweepModal] = useState(false); + const [sweepAddress, setSweepAddress] = useState(''); + const [sweepPassword, setSweepPassword] = useState(''); + const [isSweeping, setIsSweeping] = useState(false); + const [sweepResult, setSweepResult] = useState(null); + + const handleSweep = async () => { + if (!sweepAddress || !userId || !masterPassword) return; + setIsSweeping(true); + setSweepResult(null); + try { + const result = await invoke<{ success: boolean; swept_sats: number; error?: string }>('sweep_qube_wallet', { + userId, + qubeId: qube.qube_id, + sweepAddress, + password: sweepPassword || masterPassword, + }); + if (result.success) { + const bch = (result.swept_sats / 1e8).toFixed(8); + setSweepResult(`✓ Swept ${bch} BCH`); + setWalletBalance(0); + setCachedBalance(qube.qube_id, 0); + setSweepAddress(''); + } else { + setSweepResult(`✗ ${result.error || 'Sweep failed'}`); + } + } catch (err) { + setSweepResult(`✗ ${String(err)}`); + } finally { + setIsSweeping(false); + } + }; // Custom voices now come from VoiceLibraryContext (see useVoiceLibrary hook above) @@ -3372,6 +3746,51 @@ const QubeCard: React.FC = ({ qube, allQubes, onEdit, onDelete, o )}
+ + {/* Sweep button — active only when balance > 0 */} + {!showSweepModal ? ( + + ) : ( +
+ setSweepAddress(e.target.value)} + placeholder="bitcoincash:q..." + className="w-full bg-bg-primary border border-glass-border rounded px-2 py-1 text-text-primary placeholder:text-text-disabled font-mono text-[10px] mb-1 focus:outline-none" + /> +
+ + +
+ {sweepResult && ( +

+ {sweepResult} +

+ )} +
+ )}
)} diff --git a/qubes-gui/src/components/tabs/SettingsTab.tsx b/qubes-gui/src/components/tabs/SettingsTab.tsx index 39bd518..c977774 100644 --- a/qubes-gui/src/components/tabs/SettingsTab.tsx +++ b/qubes-gui/src/components/tabs/SettingsTab.tsx @@ -1,5 +1,6 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; import { GlassCard, GlassButton, GlassInput } from '../glass'; import { useAuth } from '../../hooks/useAuth'; import { useChainState } from '../../contexts/ChainStateContext'; @@ -76,6 +77,37 @@ interface MemoryConfig { relationship_weight: number; } +interface RelayPeer { + multiaddr: string; + label: string; + operator: string; + builtin: boolean; + online: boolean | null; + latency_ms: number | null; +} + +interface RelayStatus { + running: boolean; + peer_id: string | null; + multiaddr: string | null; + peer_count: number; + online_peers: number; +} + +interface RelayPreferences { + relay_enabled: boolean; + relay_listen_port: number; + relay_max_connections: number; + relay_retention_days: number; + relay_custom_peers: string[]; + p2pd_binary_path: string | null; +} + +interface EndpointPreferences { + fulcrum_nodes: string[]; + nostr_relays: string[]; +} + export const SettingsTab: React.FC = () => { const { userId, password, autoLockEnabled, autoLockTimeout, setAutoLockSettings } = useAuth(); const { invalidateCache, loadChainState } = useChainState(); @@ -125,8 +157,8 @@ export const SettingsTab: React.FC = () => { individual_anchor_threshold: 10, group_auto_anchor: true, group_anchor_threshold: 5, - auto_sync_ipfs_on_anchor: false, - auto_sync_ipfs_periodic: false, + auto_sync_ipfs_on_anchor: true, + auto_sync_ipfs_periodic: true, auto_sync_ipfs_interval: 15, }); const [loadingPreferences, setLoadingPreferences] = useState(true); @@ -219,12 +251,142 @@ export const SettingsTab: React.FC = () => { trustPersonality: true, voiceSettings: true, gpuAcceleration: true, + localModels: true, decisionIntelligence: true, security: true, celebrationSettings: true, softwareUpdates: true, + endpoints: true, }); + // Relay Node state + const [relayStatus, setRelayStatus] = useState({ running: false, peer_id: null, multiaddr: null, peer_count: 0, online_peers: 0 }); + const [relayPeers, setRelayPeers] = useState([]); + const [relayPrefs, setRelayPrefs] = useState({ relay_enabled: true, relay_listen_port: 0, relay_max_connections: 50, relay_retention_days: 7, relay_custom_peers: [], p2pd_binary_path: null }); + const [relayLoading, setRelayLoading] = useState(false); + const [relayBundleStatus, setRelayBundleStatus] = useState(null); + const [newRelayPeer, setNewRelayPeer] = useState(''); + + // Endpoints state — pre-filled with well-known defaults (overwritten by saved prefs on load) + const [endpointPrefs, setEndpointPrefs] = useState({ + fulcrum_nodes: [ + 'wss://bch.imaginary.cash:50004', + 'wss://electroncash.de:50004', + 'wss://bch.loping.net:50004', + 'wss://blackie.c3-soft.com:50004', + 'wss://electroncash.dk:50002', + 'wss://electron.jochen-hoenicke.de:51002', + 'wss://bitcoincash.network:50004', + 'wss://bch.soul-dev.com:50002', + ], + nostr_relays: [ + 'wss://relay.damus.io', + 'wss://nos.lol', + 'wss://relay.nostr.band', + 'wss://relay.snort.social', + ], + }); + const [endpointsDirty, setEndpointsDirty] = useState(false); + const [savingEndpoints, setSavingEndpoints] = useState(false); + const [endpointSaveMsg, setEndpointSaveMsg] = useState(null); + const [endpointConnStatus, setEndpointConnStatus] = useState<{ + fulcrum: { connected: number; total: number; status: boolean[] }; + nostr: { connected: number; total: number; status: boolean[] }; + } | null>(null); + const [checkingConn, setCheckingConn] = useState(false); + + const loadRelayData = async () => { + try { + const [statusRes, peersRes] = await Promise.all([ + invoke('get_relay_status', { userId }), + invoke('get_relay_peers', { userId }), + ]); + if (statusRes?.success !== false) setRelayStatus(statusRes); + if (peersRes?.success !== false && peersRes?.peers) setRelayPeers(peersRes.peers); + } catch {} + }; + + const handleStartRelay = async () => { + setRelayLoading(true); + try { + const res = await invoke('init_relay_node', { userId, password }); + if (res?.success !== false) { setRelayStatus({ ...relayStatus, running: true, ...res }); } + } catch {} + setRelayLoading(false); + await loadRelayData(); + }; + + const handleStopRelay = async () => { + setRelayLoading(true); + try { await invoke('stop_relay_node', { userId }); setRelayStatus(s => ({ ...s, running: false, peer_id: null, multiaddr: null })); } catch {} + setRelayLoading(false); + }; + + const handleAddRelayPeer = async () => { + if (!newRelayPeer.trim()) return; + await invoke('add_relay_peer', { userId, multiaddr: newRelayPeer.trim() }); + setNewRelayPeer(''); + await loadRelayData(); + }; + + const handleRemoveRelayPeer = async (multiaddr: string) => { + await invoke('remove_relay_peer', { userId, multiaddr }); + await loadRelayData(); + }; + + const handleCheckBundleUpdate = async () => { + setRelayBundleStatus('Checking...'); + try { + const res = await invoke('check_relay_bundle_update', { userId }); + setRelayBundleStatus(res?.update_available ? `Update available: ${res.latest_version}` : `Up to date (${res?.current_version || 'none'})`); + } catch (e: any) { setRelayBundleStatus(`Error: ${e}`); } + }; + + const handleUpdateBundle = async () => { + setRelayBundleStatus('Updating...'); + try { + const res = await invoke('update_relay_bundle', { userId }); + setRelayBundleStatus(res?.message || (res?.success ? 'Updated!' : 'Failed')); + } catch (e: any) { setRelayBundleStatus(`Error: ${e}`); } + }; + + const loadEndpoints = async () => { + try { + const res = await invoke('get_endpoint_preferences', { userId }); + if (res?.endpoints) setEndpointPrefs(res.endpoints); + } catch {} + }; + + const checkEndpointConn = async () => { + setCheckingConn(true); + try { + const res = await invoke('check_endpoints', { userId }); + if (res?.success !== false) { + setEndpointConnStatus({ fulcrum: res.fulcrum, nostr: res.nostr }); + } + } catch {} + setCheckingConn(false); + }; + + const handleSaveEndpoints = async () => { + setSavingEndpoints(true); + setEndpointSaveMsg(null); + try { + const prefs_json = JSON.stringify(endpointPrefs); + await invoke('update_endpoint_preferences', { userId, password, prefsJson: prefs_json }); + setEndpointsDirty(false); + setEndpointSaveMsg('Saved — changes apply on page reload'); + } catch (e: any) { setEndpointSaveMsg(`Error: ${e}`); } + setSavingEndpoints(false); + }; + + const handleResetEndpoints = async () => { + try { + const res = await invoke('reset_endpoint_preferences', { userId, password }); + if (res?.endpoints) { setEndpointPrefs(res.endpoints); setEndpointsDirty(false); setEndpointSaveMsg('Reset to defaults — changes apply on page reload'); } + } catch {} + }; + const handleChangePassword = async () => { setChangePwError(null); setChangePwSuccess(null); @@ -296,6 +458,21 @@ export const SettingsTab: React.FC = () => { const [gpuUninstalling, setGpuUninstalling] = useState(false); const [gpuError, setGpuError] = useState(null); + // Local Models state + const [ollamaModels, setOllamaModels] = useState([]); + const [isPullingModel, setIsPullingModel] = useState(null); + const [pullProgress, setPullProgress] = useState<{ status: string; completed?: number; total?: number } | null>(null); + const [newModelInput, setNewModelInput] = useState(''); + const [ttsModelStatus, setTtsModelStatus] = useState<{ + kokoro_installed: boolean; + sentence_transformers_installed: boolean; + whisper_installed: boolean; + models_dir: string; + } | null>(null); + const [isUpdatingTts, setIsUpdatingTts] = useState(false); + const [ttsUpdateResult, setTtsUpdateResult] = useState(null); + const pullListenerRef = useRef<(() => void) | null>(null); + const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; @@ -379,6 +556,98 @@ export const SettingsTab: React.FC = () => { } }, [userId]); + // Load endpoints on mount and check connectivity + useEffect(() => { + if (userId) { loadEndpoints(); checkEndpointConn(); } + }, [userId]); + + // Listen for Ollama pull progress events + useEffect(() => { + let unlisten: (() => void) | null = null; + listen<{ model: string; status: string; completed?: number; total?: number }>('ollama-pull-progress', (event) => { + if (event.payload.status === 'done') { + setPullProgress(null); + setIsPullingModel(null); + // Refresh model list after pull + checkOllamaModels(); + } else { + setPullProgress({ status: event.payload.status, completed: event.payload.completed, total: event.payload.total }); + } + }).then((fn) => { + unlisten = fn; + pullListenerRef.current = fn; + }); + return () => { + if (unlisten) unlisten(); + }; + }, []); + + const checkOllamaModels = async () => { + try { + const result = await invoke<{ running: boolean; models: string[] }>('check_ollama_status'); + if (result.running) { + setOllamaModels(result.models); + } + } catch (err) { + console.error('Failed to check Ollama models:', err); + } + }; + + const checkTtsModels = async () => { + if (!userId) return; + try { + const result = await invoke('check_local_tts_models', { userId }); + setTtsModelStatus(result); + } catch (err) { + console.error('Failed to check TTS models:', err); + } + }; + + const handlePullModel = async (modelName: string) => { + if (isPullingModel) return; + setIsPullingModel(modelName); + setPullProgress({ status: 'starting...' }); + try { + await invoke('pull_ollama_model', { modelName }); + } catch (err) { + setIsPullingModel(null); + setPullProgress(null); + alert(`Failed to pull ${modelName}: ${err}`); + } + }; + + const handleUpdateAllOllamaModels = async () => { + if (isPullingModel || ollamaModels.length === 0) return; + for (const model of ollamaModels) { + setIsPullingModel(model); + setPullProgress({ status: 'starting...' }); + try { + await invoke('pull_ollama_model', { modelName: model }); + } catch (err) { + console.error(`Failed to pull ${model}:`, err); + } + } + }; + + const handleUpdateTtsModels = async () => { + if (!userId || isUpdatingTts) return; + setIsUpdatingTts(true); + setTtsUpdateResult(null); + try { + const result = await invoke<{ success: boolean; updated: string[]; errors: string[] }>('update_local_tts_models', { userId }); + if (result.success) { + setTtsUpdateResult(`Updated: ${result.updated.join(', ') || 'already up to date'}`); + } else { + setTtsUpdateResult(`Errors: ${result.errors.join('; ')}`); + } + await checkTtsModels(); + } catch (err) { + setTtsUpdateResult(`Failed: ${err}`); + } finally { + setIsUpdatingTts(false); + } + }; + const checkGpuAcceleration = async () => { try { const result = await invoke('check_gpu_acceleration', { userId }); @@ -1191,7 +1460,7 @@ export const SettingsTab: React.FC = () => { className="w-full flex items-center justify-between text-left" >

- ⚓ Auto-Anchor + ⚓ Anchor & Backup

▼ @@ -1203,15 +1472,17 @@ export const SettingsTab: React.FC = () => { {loadingPreferences ? (

Loading...

) : ( -
- {/* Individual Chat Settings */} -
-

- Individual Chat -

+
+ + {/* BCH Anchor */} +
+

⚓ Auto-Anchor

+ + {/* Individual Chat */}
+

Individual Chat

- + {blockPreferences.individual_auto_anchor && ( + + )}
-
- {/* Group Chat Settings */} -
-

- Group Chat -

-
+ {/* Group Chat */} +
+

Group Chat

- + {blockPreferences.group_auto_anchor && ( + + )}
- {/* IPFS Sync Settings */} -
-

- IPFS Sync -

-
+ {/* IPFS Backup */} +
+

☁️ IPFS Backup (Pinata)

+ + {/* After anchor */} +

- Automatically upload .qube package to IPFS after each auto-anchor + Upload to Pinata immediately after every auto-anchor (new CID replaces old)

+
-
+
)} @@ -1775,6 +2044,158 @@ export const SettingsTab: React.FC = () => { )} + + {/* Local Models */} + + + + {!collapsedPanels.localModels && ( + <> +

+ Update AI models (Ollama) and voice/embedding models (Kokoro TTS, Sentence Transformers). +

+ + {/* Ollama Models */} +
+

🤖 AI Models (Ollama)

+ {ollamaModels.length > 0 ? ( +
+ {ollamaModels.map((model) => ( +
+
+ {model} + ✓ installed +
+ handlePullModel(model)} + disabled={isPullingModel !== null} + > + {isPullingModel === model ? 'Updating...' : 'Update'} + +
+ ))} + + {isPullingModel ? `Updating ${isPullingModel}...` : 'Update All Ollama Models'} + +
+ ) : ( +
No Ollama models installed yet.
+ )} + + {/* Pull progress */} + {pullProgress && ( +
+
+ {pullProgress.status} + {pullProgress.total && pullProgress.total > 0 && ( + + {formatBytes(pullProgress.completed ?? 0)} / {formatBytes(pullProgress.total)} + + )} +
+ {pullProgress.total && pullProgress.total > 0 && ( +
+
+
+ )} +
+ )} + + {/* Download new model */} +
+

Download a new model (e.g. llama3.2:3b)

+
+ setNewModelInput(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter' && newModelInput.trim()) { handlePullModel(newModelInput.trim()); setNewModelInput(''); } }} + placeholder="model:tag" + className="flex-1 bg-bg-primary/60 border border-glass-border rounded px-3 py-1.5 text-xs text-text-primary placeholder:text-text-disabled focus:outline-none focus:border-accent-primary" + disabled={isPullingModel !== null} + /> + { if (newModelInput.trim()) { handlePullModel(newModelInput.trim()); setNewModelInput(''); } }} + disabled={isPullingModel !== null || !newModelInput.trim()} + > + Download + +
+
+
+ + {/* Voice & Embedding Models */} +
+

🎙️ Voice & Embedding Models

+ {ttsModelStatus ? ( +
+
+ Kokoro TTS 82M + + {ttsModelStatus.kokoro_installed ? '✓ installed' : '✗ missing'} + +
+
+ Sentence Transformers + + {ttsModelStatus.sentence_transformers_installed ? '✓ installed' : '✗ missing'} + +
+
+ Whisper STT + + {ttsModelStatus.whisper_installed ? '✓ installed' : '✗ missing'} + +
+
+ ) : ( +
Click to check model status...
+ )} + + + {isUpdatingTts ? 'Re-downloading...' : 'Re-download Voice Models'} + + + {ttsUpdateResult && ( +

+ {ttsUpdateResult} +

+ )} +
+ + )} +
{/* Right Column */} @@ -2584,6 +3005,211 @@ export const SettingsTab: React.FC = () => { )}
+ {/* =========================================================== */} + {/* Endpoints Panel (relay node + network endpoints combined) */} + {/* =========================================================== */} + + + + {!collapsedPanels.endpoints && ( + <> + {/* ── RELAY NODE ─────────────────────────────────────── */} +
+

+ 📡 Relay Node + + {relayStatus.running ? `Running — ${relayStatus.online_peers}/${relayStatus.peer_count} peers` : 'Stopped'} +

+ + {relayStatus.running && relayStatus.peer_id && ( +

Peer ID: {relayStatus.peer_id}

+ )} + + {/* LOCAL RELAY controls */} +
+ + +
+

p2pd binary path (leave blank = use bundled)

+ setRelayPrefs(p => ({ ...p, p2pd_binary_path: e.target.value || null }))} + /> +
+ +
+ + + +
+ +
+ ▶ Start + ■ Stop +
+
+ + {/* SEED RELAYS */} +
+

Seed Relays (BitFaced-operated)

+
+ {[ + { label: 'US-East', id: 'QmRelayUS' }, + { label: 'EU', id: 'QmRelayEU' }, + { label: 'Asia', id: 'QmRelayAS' }, + ].map((seed) => { + const live = relayPeers.find(p => p.builtin && p.label?.includes(seed.label)); + return ( +
+ + BitFaced {seed.label} + {live?.latency_ms != null && {live.latency_ms}ms} + {live?.online === false && offline} + {!live && start relay to ping} +
+ ); + })} +
+
+ + {/* CUSTOM RELAYS */} +
+

Custom Relays

+
+ {relayPeers.filter(p => !p.builtin).map((peer, i) => ( +
+ + {peer.multiaddr} + +
+ ))} + {relayPeers.filter(p => !p.builtin).length === 0 && ( +

No custom relays added

+ )} +
+
+ setNewRelayPeer(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleAddRelayPeer()} /> + Add +
+
+ + {/* RELAY BUNDLE */} +
+

Relay Bundle

+

Update p2pd binary + relay list independently from the main app.

+
+ Check Update + Update Bundle +
+ {relayBundleStatus &&

{relayBundleStatus}

} +
+
+ + {/* ── NETWORK ENDPOINTS ──────────────────────────────── */} +
+
+

// NETWORK ENDPOINTS

+ +
+ +
+
+

FULCRUM / ELECTRUM NODES

+ {endpointConnStatus && ( + 0 ? 'text-yellow-400' : 'text-red-400'}`}> + {endpointConnStatus.fulcrum.connected}/{endpointConnStatus.fulcrum.total} + + )} + {checkingConn && !endpointConnStatus && pinging…} +
+ {/* per-line status dots overlay — shown after ping */} + {endpointConnStatus && ( +
+ {endpointPrefs.fulcrum_nodes.map((url, i) => ( +
+ + {url} +
+ ))} +
+ )} +