From 422647e7898b8df67cd11f8fb060aec23dec84aa Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 14 Oct 2026 22:20:15 +0200 Subject: feat(cli): meshbay-cli, a command-line client for members Signs in to the hub, lists the account's groups and their folders, and downloads and uploads files directly with each group's node, end-to-end encrypted. With no argument it opens a two-pane interactive mode (Textual). Session and device keys are kept encrypted under the system's key storage, or a 0600 key file where there is none; node keys are pinned at first contact; a new machine links as a device. Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a man page generated from the parser) and frozen next to meshbay-node in the full Windows installer, not the light one, with a self-test the build runs. client/keys.py ports keyderive.js and is LGPL like it. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-cli/src/meshbay_cli/client/names.py | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/meshbay-cli/src/meshbay_cli/client/names.py (limited to 'packages/meshbay-cli/src/meshbay_cli/client/names.py') diff --git a/packages/meshbay-cli/src/meshbay_cli/client/names.py b/packages/meshbay-cli/src/meshbay_cli/client/names.py new file mode 100644 index 0000000..f6d7d32 --- /dev/null +++ b/packages/meshbay-cli/src/meshbay_cli/client/names.py @@ -0,0 +1,69 @@ +""" +Names crossing between a group and this machine, in both directions. + +**Uploading**, the node decides: `SAFE_UPLOAD_NAME` and the Explorer-active types +in `meshbay_node/roots.py`. This is a copy of that rule, checked before anything +is sent so a folder holding one refused name is refused whole instead of +arriving with holes in it — as the Files tab does with its own copy. +`test_names.py` holds this copy to the node's answers. + +**Downloading**, a name from the index is someone else's text. It is made +writable on this platform (`meshbay_common.paths.sanitize_for_download`, the +same rule the browser applies), and the resolved path must stay under the +destination: checked after resolution, never by comparing strings. +""" + +import re +from pathlib import Path + +from meshbay_common.paths import fold, sanitize_for_download + +UPLOAD_NAME = re.compile( + r"^[^\W_]" + r"[\w .\-()\[\]'’,&+#@]{0,127}" + r"(? str | None: + if not UPLOAD_NAME.match(name): + return ("starts with something other than a letter or digit, ends with a space " + "or a dot, holds a character the node does not accept, or is longer " + "than 128 characters") + lower = name.lower() + if not is_dir and (lower in SHELL_ACTIVE_NAMES or lower.endswith(SHELL_ACTIVE_SUFFIXES)): + return "a file type Windows Explorer acts on by itself; no node accepts it" + return None + + +def same_name(a: str, b: str) -> bool: + """As the node and the Files tab compare names in a folder: without case.""" + return fold(a) == fold(b) + + +class UnsafeName(Exception): + pass + + +def local_path(dest: Path, parts: list[str]) -> tuple[Path, bool]: + """ + Where a remote file or folder lands under `dest`, and whether a name had to + be changed to be written here. + """ + renamed = False + out = dest + for part in parts: + safe = sanitize_for_download(part) + if safe in (".", ".."): + safe = "unnamed" + renamed |= safe != part + out = out / safe + base = dest.resolve() + try: + out.resolve().relative_to(base) + except ValueError: + raise UnsafeName(f"{'/'.join(parts)!r} would land outside {dest}") from None + return out, renamed -- cgit v1.2.3