From 8f6e2f724fd24a077de11d4a3b3ae069d369324d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 14 Aug 2026 01:27:57 +0200 Subject: feat(node): operator surface — member list, invite, revoke, unpin over SSH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 one-time code; the node wraps the key when they connect, so nobody has to be online then member revoke stop serving them the key member unpin 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: " bug in the not-found path turned up. Tests: 89 node here (roster, endpoints, CLI routing, TTL config). Co-Authored-By: Claude Opus 5 --- packages/meshbay-node/src/meshbay_node/config.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'packages/meshbay-node/src/meshbay_node/config.py') 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: -- cgit v1.2.3