aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/config.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-14 01:27:57 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-14 01:27:57 +0200
commit8f6e2f724fd24a077de11d4a3b3ae069d369324d (patch)
tree25b06d11f74b7b73e2056e1bb75c1b64af6c9650 /packages/meshbay-node/src/meshbay_node/config.py
parentf15efd23f66c521ca9206789482bb38e7326eeb4 (diff)
downloadmeshbay-8f6e2f724fd24a077de11d4a3b3ae069d369324d.tar.gz
feat(node): operator surface — member list, invite, revoke, unpin over SSH
A node admits people from its own roster, and until now a headless operator had no way to put anyone on it: pairing worked from the CLI, everything else needed a browser on a machine that does not have one. Absorbs milestones 14.3/14.4. member list who is admitted, role, status, when and how pinned member invite <username> one-time code; the node wraps the key when they connect, so nobody has to be online then member revoke <username> stop serving them the key member unpin <username> forget the pin so they can pair again after a reset All of it goes through the daemon's loopback API with the per-run session token (11.5.3) — _daemon_api() in daemon.py, which also replaced three hand-rolled urllib blocks. `status` deliberately still reads the keystore, config and roster directly, so it works while the daemon is stopped. Two things the commands say out loud, because getting them wrong is silent: - revoke ends by telling the operator to rotate the key. The ex-member stops receiving it on their next connection, but they hold the current one, and "revoked" reads like it took the key back. - revoke/unpin refuse a username the roster does not know instead of acting on nobody. A typo must not look like success. Code lifetimes now differ by what the act is: 7 days for an invitation, which crosses a human conversation and gets answered whenever someone reads their messages, and 24 h for operator pairing, which is typed during the SSH session that printed it. Both configurable ([node] invite_ttl_hours, pair_ttl_hours). A day was long enough for the second and not for the first — a code that dies over a weekend means finding a browser to issue another one. The roster is also in the local admin UI, escaped: usernames come from the hub and land on the page that can re-key groups and read the audit log, so H2's rule covers them exactly as it covers filenames. Verified by driving the real CLI against a stub daemon over a socket, which is how the "known: <nothing>" bug in the not-found path turned up. Tests: 89 node here (roster, endpoints, CLI routing, TTL config). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/config.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/config.py14
1 files changed, 14 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py
index 04cb1d3..f3752ea 100644
--- a/packages/meshbay-node/src/meshbay_node/config.py
+++ b/packages/meshbay-node/src/meshbay_node/config.py
@@ -29,6 +29,11 @@ username = "myusername"
quic_port = 19010 # QUIC (MNP) — LAN, port-forwarded, hub-less direct access
ui_port = 18000 # local admin UI (127.0.0.1 only)
+# One-time codes. An invitation waits for someone to read their messages; an
+# operator pairing code is typed during the SSH session that printed it.
+invite_ttl_hours = 168 # 7 days
+pair_ttl_hours = 24
+
# Browser and native clients reach this node over WebRTC DataChannel via hub
# signaling — no inbound port to open. QUIC is the optional direct path.
@@ -70,6 +75,11 @@ class HubConfig:
class NodeConfig:
quic_port: int = 19010
ui_port: int = 18000
+ # How long a one-time code stays usable. Invitations travel through a human
+ # conversation and are answered days later; operator pairing happens during
+ # the SSH session that printed it.
+ invite_ttl_hours: int = 168 # 7 days
+ pair_ttl_hours: int = 24
@dataclass
@@ -129,6 +139,10 @@ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config:
# in Phase 11.5 (findings C1, C6). Regenerate node.toml with `meshbay-node init`.
cfg.node.quic_port = nd.get("quic_port", cfg.node.quic_port)
cfg.node.ui_port = nd.get("ui_port", cfg.node.ui_port)
+ cfg.node.invite_ttl_hours = int(
+ nd.get("invite_ttl_hours", cfg.node.invite_ttl_hours))
+ cfg.node.pair_ttl_hours = int(
+ nd.get("pair_ttl_hours", cfg.node.pair_ttl_hours))
# Multi-group: [[groups]] array
if "groups" in raw: