aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/cli/members.py25
-rw-r--r--packages/meshbay-node/src/meshbay_node/cli/parser.py8
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops/members.py17
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py6
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py2
6 files changed, 32 insertions, 28 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/cli/members.py b/packages/meshbay-node/src/meshbay_node/cli/members.py
index ba68056..5d26bd0 100644
--- a/packages/meshbay-node/src/meshbay_node/cli/members.py
+++ b/packages/meshbay-node/src/meshbay_node/cli/members.py
@@ -62,18 +62,15 @@ def member(args) -> None:
print("which is what turning the old switch off meant.")
sys.exit(1)
- if not args.target:
- 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.
+ # A link for someone who may have no account yet, redeemable by the
+ # first account that opens it. The code and the ticket are both in
+ # it, so it goes to them and to nobody else — the CLI mails nothing.
+ # The address, if given, only labels the link in the owner's list.
group_id = _resolve_group(cfg, args.group)
+ query = f"?email={quote(args.target)}" if args.target else ""
out = _daemon_api(
- cfg, f"/api/groups/{group_id}/invite-links?email={quote(args.target)}",
- method="POST")
+ cfg, f"/api/groups/{group_id}/invite-links{query}", 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")
@@ -81,11 +78,15 @@ def member(args) -> None:
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.")
+ print("Send it yourself, by any messaging app. It works once, for seven")
+ print("days, for whoever opens it first: they create their account or")
+ print("sign in, and land in the group without typing a code.")
return
+ if not args.target:
+ print(f"usage: meshbay-node member {sub} <username>")
+ sys.exit(1)
+
if sub == "invite":
group_id = _resolve_group(cfg, args.group)
out = _daemon_api(
diff --git a/packages/meshbay-node/src/meshbay_node/cli/parser.py b/packages/meshbay-node/src/meshbay_node/cli/parser.py
index c1f15be..29a9f63 100644
--- a/packages/meshbay-node/src/meshbay_node/cli/parser.py
+++ b/packages/meshbay-node/src/meshbay_node/cli/parser.py
@@ -52,8 +52,8 @@ def build_parser() -> argparse.ArgumentParser:
"install|remove|start|stop|status for autostart and "
"for service")
parser.add_argument("target", nargs="?",
- help="username for member invite|revoke|unpin (an e-mail address with "
- "--link, a link id for member cancel); group name "
+ help="username for member invite|revoke|unpin (an optional e-mail label "
+ "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")
@@ -73,8 +73,8 @@ def build_parser() -> argparse.ArgumentParser:
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")
+ help="member invite: an invitation link, for someone who "
+ "may have no account yet (valid 7 days, single use)")
parser.add_argument("--writable", action="store_true", default=None,
dest="writable",
help="root accepts member uploads (root add/set)")
diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py
index 06d3503..49bb920 100644
--- a/packages/meshbay-node/src/meshbay_node/hub_client.py
+++ b/packages/meshbay-node/src/meshbay_node/hub_client.py
@@ -266,7 +266,7 @@ class HubClient:
async def create_invite_link(self, group_id: str, email: str, expires_at: str,
node_invite_id: str) -> dict:
"""
- The hub's half of an invitation link: a ticket bound to `email`.
+ The hub's half of an invitation link: a ticket, labelled `email` if any.
Never with `send_email`: the hub refuses mail to a node token, and the
CLI mails nothing — the operator sends the link (docs/USERGUIDE.md §7).
diff --git a/packages/meshbay-node/src/meshbay_node/ops/members.py b/packages/meshbay-node/src/meshbay_node/ops/members.py
index 2fd3899..b5da9c0 100644
--- a/packages/meshbay-node/src/meshbay_node/ops/members.py
+++ b/packages/meshbay-node/src/meshbay_node/ops/members.py
@@ -159,15 +159,13 @@ async def create_link_invite(state: dict, group_id: str, *,
Nothing is registered on the hub here, unlike `create_invite`: there is no
account to register yet. The hub half is a ticket the inviter's client asks
- the hub for, bound to the invitee's address (docs/MESHBAY_DESIGN.md §7.3).
+ the hub for, redeemable by the first account that opens the link
+ (docs/MESHBAY_DESIGN.md §7.3). Seven days, whatever `invite_ttl_hours` says.
"""
roster = _roster(state)
_group_ctx(state, group_id)
- config = state.get("config")
- ttl = (config.node.invite_ttl_hours if config else 168) * 3600
try:
- code, invite_id, expires = await roster.create_link_invite(
- group_id, created_by, ttl=ttl)
+ code, invite_id, expires = await roster.create_link_invite(group_id, created_by)
except LinkInviteLimit as e:
raise OpError(str(e), status=429) from e
log.info("Invitation link issued: group=%s invite=%s", group_id[:8], invite_id[:8])
@@ -199,11 +197,12 @@ def _invite_url(hub_url: str, group_id: str, ticket: str, node_pk_b64: str, code
return f"{origin}/#/invite?v=1&g={group_id}&t={ticket}&n={n}&c={code}"
-async def create_link_invitation(state: dict, group_id: str, email: str, *,
+async def create_link_invitation(state: dict, group_id: str, email: str = "", *,
created_by: str = "local-cli") -> dict:
"""
A whole invitation link, from the operator's own machine: the node's code,
- then the hub's ticket bound to `email`, then the link.
+ then the hub's ticket, then the link. `email` is optional and only labels
+ the link in the owner's list.
In that order because the ticket names the code's handle. A ticket the hub
refuses takes the code back with it — a code nobody can reach the node with
@@ -211,8 +210,8 @@ async def create_link_invitation(state: dict, group_id: str, email: str, *,
the operator sends the link.
"""
email = (email or "").strip()
- if "@" not in email:
- raise OpError("An invitation link is bound to an e-mail address", status=422)
+ if email and "@" not in email:
+ raise OpError("Not an e-mail address", status=422)
hub = _hub(state)
sk_node = state.get("sk_node")
if sk_node is None:
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index 80bf16d..8144373 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -54,6 +54,10 @@ CODE_LEN = 8 # 8 × 5 bits = 40 bits of entropy
# node-wide lockout.
DEFAULT_INVITE_TTL = 7 * 24 * 3600 # seconds — member invitations
DEFAULT_PAIR_TTL = 24 * 3600 # seconds — operator pairing
+# An invitation link is a bearer secret that may travel through any messaging
+# service, so its lifetime is fixed rather than the operator's setting: the
+# hub clamps its ticket to the same seven days.
+LINK_INVITE_TTL = 7 * 24 * 3600
# A device-add code is read off one screen and typed into another, in one
# sitting. An hour is comfort, not security: the code is bound to the requesting
# keys by its hash, so a longer window widens nothing an attacker can use.
@@ -1109,7 +1113,7 @@ class Roster:
return code
async def create_link_invite(
- self, group_id: str, created_by: str, ttl: int = DEFAULT_INVITE_TTL,
+ self, group_id: str, created_by: str, ttl: int = LINK_INVITE_TTL,
) -> tuple[str, str, str]:
"""
Issue a code bound to no account: `(code, invite_id, expires_at)`.
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index eabe97a..090eb21 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -325,7 +325,7 @@ def create_ui_app(state: dict) -> FastAPI:
# Both halves, node and hub, for the CLI: an operator at the machine gets a
# whole link, not a code without a ticket.
@app.post("/api/groups/{group_id}/invite-links")
- async def create_link_invite(group_id: str, email: str):
+ async def create_link_invite(group_id: str, email: str = ""):
return await _op(lambda: ops.create_link_invitation(state, group_id, email))
@app.delete("/api/groups/{group_id}/invite-links/{invite_id}")