From 988a0eecd522431980174910db28e45a63ff2710 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 15 Oct 2026 12:32:24 +0200 Subject: feat(cli): account, rm -r and mkdir, Tab completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit account shows the account and does what the sign-up form and the Profile page do: register (the passphrase floor and the hub's proof of work checked here, the recovery key printed once), verify, change the passphrase, reset a forgotten one, change the e-mail address, sign out everywhere, delete. A new passphrase carries the copy of the account's keys each node keeps for browsers first — opened under the old M, the same keys sealed under the new one — and only then tells the hub; a refusal sets the copies back. A reset restores them from the recovery copy. The connections that do it never make keys nor ask for a device approval. This machine keeps the M it changed away from, so a node out of reach is carried at its next connection; a copy under an M it does not know is no longer rewritten, which locked the browsers out after a passphrase changed elsewhere. rm -r removes a folder and all it holds, from the node's operator key: the files, then the folders deepest first, one confirmation that counts them. mkdir makes folders, --parents the missing ones (not -p, the passphrase). completion bash|powershell prints the script that makes Tab complete actions, verbs, options and the paths of the groups this machine is signed in to, cached two minutes; the deb and rpm packages install the bash one. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-cli/tests/fake_node.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-cli/tests/fake_node.py') diff --git a/packages/meshbay-cli/tests/fake_node.py b/packages/meshbay-cli/tests/fake_node.py index f9c552c..552bd39 100644 --- a/packages/meshbay-cli/tests/fake_node.py +++ b/packages/meshbay-cli/tests/fake_node.py @@ -23,7 +23,7 @@ from fake_hub import USER_ID from meshbay_cli.client.keys import Identity from meshbay_cli.client.session import GroupSession, NodeRefusal from meshbay_common import chatbox -from meshbay_common.adminop import OP_FILE_DELETE, admin_transcript +from meshbay_common.adminop import OP_DIR_DELETE, OP_FILE_DELETE, admin_transcript from meshbay_common.device import ( device_add_transcript, device_code_hash, @@ -322,11 +322,38 @@ class FakeChannel: "subject": msg["file_id"], "nonce": base64.b64encode(nonce).decode(), "ts": 1_700_000_000, "node_pk": NODE_PK, "group_id": GROUP_ID} + def _dir_delete(self, msg): + """Empty folders only, the operator's only, never a root — the node's rule.""" + n, path = self.node, msg["dir"].strip("/") + if path not in n.dirs or "/" not in path: + return {"type": "error", "detail": "Invalid directory"} + if any(d.startswith(path + "/") for d in n.dirs) or \ + any(e["path"] == path or e["path"].startswith(path + "/") + for e in n.files.values()): + return {"type": "error", "detail": "Directory is not empty"} + if n.role != "operator": + return {"type": "error", "detail": "No authorized key for deletion"} + op_id, nonce = secrets.token_hex(8), secrets.token_bytes(32) + n.admin_ops[op_id] = {"op": OP_DIR_DELETE, "subject": path, "nonce": nonce, + "ts": 1_700_000_000} + return {"type": "admin_challenge", "op_id": op_id, "op": OP_DIR_DELETE, + "subject": path, "nonce": base64.b64encode(nonce).decode(), + "ts": 1_700_000_000, "node_pk": NODE_PK, "group_id": GROUP_ID} + def _admin_response(self, msg): n = self.node op = n.admin_ops.pop(msg["op_id"], None) if op is None: return {"type": "error", "detail": "No pending admin operation"} + if op.get("op") == OP_DIR_DELETE: + transcript = admin_transcript(op=OP_DIR_DELETE, node_pk_b64=NODE_PK, + group_id=GROUP_ID, subject=op["subject"], + nonce=op["nonce"], ts=op["ts"]) + if not (_signed_by_a_device(n, transcript, msg["signature"]) + and n.role == "operator"): + return {"type": "error", "detail": "Signature verification failed"} + n.dirs.discard(op["subject"]) + return {"type": "dir_delete_ack", "dir": op["subject"]} entry = n.files.get(op["subject"]) transcript = admin_transcript(op=OP_FILE_DELETE, node_pk_b64=NODE_PK, group_id=GROUP_ID, subject=op["subject"], -- cgit v1.2.3