aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/nodes.py33
1 files changed, 32 insertions, 1 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,