aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/cli/setup.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/cli/setup.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/cli/setup.py195
1 files changed, 195 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/cli/setup.py b/packages/meshbay-node/src/meshbay_node/cli/setup.py
new file mode 100644
index 0000000..b2a8e83
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/cli/setup.py
@@ -0,0 +1,195 @@
+"""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 <name> --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