diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-23 18:16:02 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-23 18:16:02 +0200 |
| commit | 1d169141f3a5542efda2f7fdb0bb88308c06191b (patch) | |
| tree | 86162905ad819d8c9dd325ccf9629bcdacc50bbf /packages/meshbay-node/src/meshbay_node/daemon.py | |
| parent | 35a7764db3f58a93c32206cb3ce74bb2f03967e7 (diff) | |
| download | meshbay-1d169141f3a5542efda2f7fdb0bb88308c06191b.tar.gz | |
feat(node): member invite --link and member cancel in the CLI
The CLI makes both halves itself — the node's code, then the hub's
ticket bound to the address — and prints the link; a refused ticket
takes the code back, and cancel takes back both. The CLI never asks the
hub to mail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/daemon.py | 42 |
1 files changed, 39 insertions, 3 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 34d186a..a3bde8f 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -33,6 +33,7 @@ import sys import time from dataclasses import asdict, replace from pathlib import Path +from urllib.parse import quote import uvicorn from meshbay_common import MNP_VERSION @@ -1959,7 +1960,7 @@ def main() -> None: help="init: provision config + keystore | reset: erase all " "node state | status: node state and keys " "| operator pair: pair a " - "browser with this node | member list|invite|revoke|unpin " + "browser with this node | member list|invite|cancel|revoke|unpin " "| group list|add|remove " "| root list|add|remove|set|eject|plug " "| gek init|rotate | file list|rm " @@ -1993,7 +1994,8 @@ def main() -> None: "install|remove|start|stop|status for autostart and " "for service") parser.add_argument("target", nargs="?", - help="username for member invite|revoke|unpin; group name " + help="username for member invite|revoke|unpin (an e-mail address with " + "--link, a link id for member cancel); group name " "for group add; file id for file rm; identifier for " "denylist clear; download cap for transfers set; " "size in GB for transfers max-size") @@ -2012,6 +2014,9 @@ def main() -> None: help="Config file path") parser.add_argument("--group", default=None, help="group id (optional if only one is configured)") + parser.add_argument("--link", action="store_true", + help="member invite: an invitation link for this e-mail " + "address, for someone who may have no account yet") parser.add_argument("--writable", action="store_true", default=None, dest="writable", help="root accepts member uploads (root add/set)") @@ -2383,6 +2388,26 @@ def main() -> None: print(f"usage: meshbay-node member {sub} <username>") sys.exit(1) + if sub == "invite" and args.link: + # A link for someone who may have no account yet, bound to their + # address on the hub. The code and the ticket are both in it, so + # it goes to them and to nobody else — the CLI mails nothing. + group_id = _resolve_group(cfg, args.group) + out = _daemon_api( + cfg, f"/api/groups/{group_id}/invite-links?email={quote(args.target)}", + method="POST") + from meshbay_node.roster import write_code_file + write_code_file(cfg.data_dir, out["link"], out.get("expires_at", ""), + name="invite-link") + print(f"INVITATION LINK {out['link']}") + print(f"valid until {out.get('expires_at', '?')}") + print(f"cancel with meshbay-node member cancel {out['invite_id']}") + print() + print(f"Send it to {args.target} yourself. It works once, and only for an") + print("account registered with that address: they open it, create their") + print("account or sign in, and land in the group without typing a code.") + return + if sub == "invite": group_id = _resolve_group(cfg, args.group) out = _daemon_api( @@ -2402,6 +2427,17 @@ def main() -> None: print(f"also written to {path}") return + if sub == "cancel": + # Takes back a link that has not been used, on the node and the hub. + group_id = _resolve_group(cfg, args.group) + out = _daemon_api( + cfg, f"/api/groups/{group_id}/invite-links/{quote(args.target)}", + method="DELETE") + print(f"invitation link {args.target[:8]} cancelled") + if not out.get("hub", True): + print("The hub's half could not be reached; it expires on its own.") + return + # revoke and unpin both name a person; the daemon resolves the account. # It tries its own roster first and falls back to the hub, so a node that # pinned someone before invitations carried a name is still manageable. @@ -2432,7 +2468,7 @@ def main() -> None: print(f"issue a code: meshbay-node member invite {args.target}") return - print("usage: meshbay-node member list|invite|revoke|unpin") + print("usage: meshbay-node member list|invite|cancel|revoke|unpin") sys.exit(1) if args.command in ("gek-init", "gek"): |