diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/nodes.py | 33 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/users.py | 6 |
2 files changed, 35 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py index 67e65f2..83b60f2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py @@ -8,7 +8,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.exceptions import InvalidSignature from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel -from sqlalchemy import select +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub.auth import issue_access_token @@ -89,6 +89,22 @@ class NodeAnnounceRequest(BaseModel): signature: str | None = None # base64 Ed25519 over the announce message +# How many distinct node keys one account may announce. +# +# M8 closed the half of this that was about *whose* key it is: the announcer now +# proves possession. What it did not close is *how many*. Each new key is a row +# in `nodes` plus a row in the IP log, and the IP log is kept for a year — so an +# account in a loop writes a year of storage on somebody else's disk, having paid +# only for the signatures. +# +# Ten is past what the feature is for. A node is a machine left running: a +# desktop, a laptop, a box in a cupboard, a second home. Someone who genuinely +# reaches it deletes one, which is a thing the operator surface already does — +# and an account that wants an eleventh *identity* rather than an eleventh +# machine is the case this refuses. +MAX_NODES_PER_ACCOUNT = 10 + + @router.post("/announce", status_code=201) async def announce_node( body: NodeAnnounceRequest, @@ -146,6 +162,21 @@ async def announce_node( await db.commit() return {"node_id": node.id} + # Counted only where a row is actually added: re-announcing a key this + # account already holds takes the branch above and must keep working at the + # ceiling, or a node that has reached it can never refresh its address again. + held = (await db.execute( + select(func.count()).select_from(Node) + .where(Node.user_id == current_user.id))).scalar() or 0 + if held >= MAX_NODES_PER_ACCOUNT: + db.add(IPLog(user_id=current_user.id, event="node_announce_refused", + ip_address=seen_from, detail=f"{held} nodes")) + await db.commit() + raise HTTPException( + status_code=409, + detail=f"This account already has {held} nodes, which is the limit of " + f"{MAX_NODES_PER_ACCOUNT}. Remove one you no longer run.") + node = Node( user_id=current_user.id, pk_node=body.pk_node, diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index 7b2fc8c..0394f53 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -107,7 +107,7 @@ class RegisterRequest(BaseModel): email: str password: str | None = None # deprecated — legacy native clients auth_key: str | None = None # PBKDF2-derived, new clients - # Client-generated account recovery key (docs/auth-confirm.md §4.4). Pure + # Client-generated account recovery key (docs/MESHBAY_DESIGN.md §3.6). Pure # pass-through: appended to the verification e-mail so the user's mailbox # backs it up, then dropped. Never written to any table, never logged. recovery_key: str | None = None @@ -901,7 +901,7 @@ async def verify_email_change( # ── Passphrase change (Flow A) ────────────────────────────────────────────── # -# docs/auth-confirm.md §3. The passphrase derives two independent values on the +# docs/MESHBAY_DESIGN.md §3.6. The passphrase derives two independent values on the # client: auth_key (verified here) and bundle_key (AES-GCM key for the per-node # identity bundles, which live on nodes and never on the hub). The client # re-wraps those bundles from the old bundle_key to the new one on every @@ -1023,7 +1023,7 @@ async def change_password( # ── Passphrase reset (Flow B) ────────────────────────────────────────────── # -# docs/auth-confirm.md §4.2. An e-mail code re-opens hub login for someone who +# docs/MESHBAY_DESIGN.md §3.6. An e-mail code re-opens hub login for someone who # has lost their passphrase. It recovers no group content — that needs the # recovery key, which the client applies on its own after the reset. # reset-request never reveals whether an account exists. |