"""Provisioning a node and taking it back down: init, reset, calibrate-argon2.""" import sys from meshbay_node.config import DEFAULT_CONFIG_PATH, Config, load_config from meshbay_node.keystore import create_keystore, load_keystore from meshbay_node.platform import chmod_private, config_dir, data_dir, state_dir def calibrate_argon2(target_ms: int = 500) -> None: """Benchmark Argon2id and suggest parameters targeting ~target_ms.""" import os import time print(f"Calibrating Argon2id (target: {target_ms}ms) ...") salt = os.urandom(16) for mem in [65536, 131072, 262144, 524288]: from cryptography.hazmat.primitives.kdf.argon2 import Argon2id t0 = time.perf_counter() Argon2id(salt=salt, length=32, iterations=3, lanes=4, memory_cost=mem).derive(b"benchmark") elapsed_ms = (time.perf_counter() - t0) * 1000 print(f" memory_cost={mem:>7} ({mem//1024:>4}MB): {elapsed_ms:.0f}ms", end="") if abs(elapsed_ms - target_ms) < target_ms * 0.3: print(" ← recommended") else: print() print("Set memory_cost in meshbay_common/crypto.py: ARGON2_MEMORY_COST") def init(args) -> None: cfg_path = args.config or DEFAULT_CONFIG_PATH cfg_dir = cfg_path.parent cfg_dir.mkdir(parents=True, exist_ok=True) from meshbay_node.platform import install_node_env env_written = install_node_env(cfg_dir) if env_written: print(f"Wrote {env_written} (packaged defaults).") hub_url = args.hub_url username = args.username if not hub_url: hub_url = input("Hub URL [https://meshbay.org]: ").strip() or "https://meshbay.org" if not username: username = input("Hub username: ").strip() if not username: print("Username is required.") sys.exit(1) if cfg_path.exists(): existing = cfg_path.read_text(encoding="utf-8") import re as _re m = _re.search(r'username\s*=\s*"([^"]*)"', existing) existing_user = m.group(1) if m else "" if existing_user and existing_user != "myusername" and existing_user != username: print(f"Config already exists with username {existing_user!r}.") print("This node belongs to another operator. Use 'meshbay-node reset' first.") sys.exit(1) if existing_user in ("", "myusername"): updated = _re.sub( r'(username\s*=\s*)"[^"]*"', rf'\1"{username}"', existing) updated = _re.sub( r'(url\s*=\s*)"[^"]*"', rf'\1"{hub_url}"', updated, count=1) cfg_path.write_text(updated, encoding="utf-8", newline="\n") print(f"Config updated: username={username}, hub={hub_url}") else: print(f"Config already exists: {cfg_path}") else: unlock_file = cfg_dir / "unlock.key" toml_lines = [ "[hub]", f'url = "{hub_url}"', f'username = "{username}"', "", "[node]", "quic_enabled = false # QUIC direct path; no client uses it yet", "quic_port = 19010", "ui_port = 18000", "", "[keystore]", # Forward slashes: a Windows path in a TOML basic string is a # parse error (`\U`, `\a`, ... are escape sequences). f'unlock_file = "{unlock_file.as_posix()}"', "", ] cfg_path.write_text("\n".join(toml_lines) + "\n", encoding="utf-8", newline="\n") chmod_private(cfg_path) print(f"Config written to {cfg_path}") unlock_file = cfg_dir / "unlock.key" if not unlock_file.exists(): import secrets key = secrets.token_urlsafe(32) unlock_file.write_text(key + "\n", encoding="utf-8", newline="\n") chmod_private(unlock_file) print(f"Unlock key created: {unlock_file}") cfg = load_config(cfg_path) if cfg.keystore.path.exists(): print(f"Keystore already exists: {cfg.keystore.path}") keys = load_keystore( path=cfg.keystore.path, unlock_file=cfg.keystore.unlock_file) else: keys = create_keystore( path=cfg.keystore.path, unlock_file=cfg.keystore.unlock_file) print(f"Keystore created: {cfg.keystore.path}") print(f"Node key: {keys.pk_ed25519_b64}") print() print("Next steps:") print(f" 1. Link this node key on {hub_url} → Settings → Link Node") if sys.platform == "win32": print(" 2. meshbay-node autostart install (run at each sign-in)") print(" — or just: meshbay-node (start it now, this session)") else: print(" 2. systemctl --user enable --now meshbay-node") print(" 3. meshbay-node group add --dir /path/to/files") print(" 4. meshbay-node gek init") print(" 5. meshbay-node operator pair") return def reset(args) -> None: import shutil config_dir_ = config_dir() data_dir_ = data_dir() state_dir_ = state_dir() items = [] for d in (config_dir_, data_dir_): if d.exists(): for child in sorted(d.iterdir()): items.append(child) if not items: print("Nothing to reset — no node state found.") return print("This will permanently erase all node state:") for p in items: print(f" {p}") print() print("WARNING: a new keystore means a new identity. All group") print("memberships, operator pairings, and invitations are lost.") if not args.yes: answer = input("\nProceed? [y/N] ").strip().lower() if answer != "y": print("Aborted.") return import json as _json import subprocess as _sp import urllib.error import urllib.request token_file = data_dir_ / "ui-token" if token_file.exists(): try: cfg = Config(config_dir_ / "node.toml") tok = token_file.read_text(encoding="utf-8").strip() url = (f"http://127.0.0.1:{cfg.node.ui_port}" f"/api/unlink?t={tok}") req = urllib.request.Request(url, method="DELETE") with urllib.request.urlopen(req, timeout=5) as r: _json.loads(r.read()) print("Unlinked node key from hub.") except Exception: print("Could not unlink from hub (daemon not reachable).") if sys.platform == "win32": from meshbay_node.platform import autostart_remove, service_remove autostart_remove() service_remove() # no-op, silently, if not elevated or not installed else: _sp.run(["systemctl", "--user", "disable", "--now", "meshbay-node"], capture_output=True) for d in (config_dir_, data_dir_): if d.exists(): shutil.rmtree(d) print(f"Removed {d}") if state_dir_.is_dir(): shutil.rmtree(state_dir_) print(f"Removed {state_dir_}") print("Node state erased. Run 'meshbay-node init' to start over.") return def calibrate(args) -> None: calibrate_argon2() return