Add community node socket: devices, tickets, consent gate, kill switch

Phase 3 of plan.md. Device registration with one-time secrets, scoped single-use
socket tickets, the lock-owner WS /nodes/ws with handshake and heartbeat eviction,
a single batch ConsentGate, and NodesService which disconnects nodes within one tick
when consent, device or the nodes_enabled kill switch changes. A reconcile of 2000
sessions measured about 110 ms after replacing per-session queries with batches.
Devii may withdraw but never grant bandwidth_sharing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 09:14:53 +02:00
co-authored by Claude Sonnet 5.5
parent fad41d0fbe
commit 42b81271b8
41 changed files with 2166 additions and 22 deletions
+50
View File
@@ -0,0 +1,50 @@
# retoor <retoor@molodetz.nl>
import time
import pytest
from fastapi.testclient import TestClient
from devplacepy.database import get_setting, get_table, set_consent, set_setting
from devplacepy.database.nodes import save_node_settings
from devplacepy.main import app
from devplacepy.services.manager import service_manager
from devplacepy.utils import register_account
_counter = [0]
def unique(prefix: str) -> str:
_counter[0] += 1
return f"{prefix}{int(time.time() * 1000)}{_counter[0]}"
def make_member(*, consent=True, enabled=True, age_band="adult", role="Member") -> dict:
name = unique("nodemb")
uid, _, _ = register_account(
name, f"{name}@t.dev", "secret123", age_band=age_band, accepted_terms=True
)
if role != "Member":
get_table("users").update({"uid": uid, "role": role}, ["uid"])
if consent:
set_consent("user", uid, "bandwidth_sharing", True)
if enabled:
save_node_settings(uid, {"enabled": True})
row = get_table("users").find_one(uid=uid)
return {"uid": uid, "username": name, "headers": {"X-API-KEY": row["api_key"]}}
@pytest.fixture(scope="module")
def client():
previous = get_setting("nodes_enabled", "0")
set_setting("nodes_enabled", "1")
with TestClient(app) as test_client:
service_manager.set_lock_owner(True)
yield test_client
service_manager.set_lock_owner(False)
set_setting("nodes_enabled", previous)
@pytest.fixture
def member(client):
return make_member()
+105
View File
@@ -0,0 +1,105 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_setting, set_setting
from devplacepy.database.nodes import LIMITS, find_device_by_secret
from tests.api.nodes.conftest import make_member
def create(client, who, **body):
payload = {"label": "Home laptop", "kind": "native", **body}
return client.post("/nodes/devices", json=payload, headers=who["headers"])
def test_guests_get_401(client):
assert client.get("/nodes/devices").status_code == 401
assert client.post("/nodes/devices", json={"label": "x"}).status_code == 401
assert client.post("/nodes/devices/x/rotate").status_code == 401
assert client.post("/nodes/devices/x/delete").status_code == 401
def test_registration_returns_the_secret_once_and_never_caches_it(client, member):
response = create(client, member)
assert response.status_code == 201
assert response.headers["cache-control"] == "no-store"
body = response.json()
assert body["secret"].startswith("dpn_")
assert body["device"]["label"] == "Home laptop"
assert "secret_hash" not in body["device"] and "ip_hash" not in body["device"]
assert find_device_by_secret(body["secret"])["uid"] == body["device"]["uid"]
listing = client.get("/nodes/devices", headers=member["headers"]).json()
assert [item["uid"] for item in listing["devices"]] == [body["device"]["uid"]]
assert listing["max_devices"] == LIMITS.max_devices_per_user
assert "secret" not in str(listing)
def test_registration_needs_consent_and_enablement(client):
no_consent = make_member(consent=False)
refused = create(client, no_consent)
assert refused.status_code == 403
assert refused.json()["error"]["reason"] == "no_consent"
not_enabled = make_member(enabled=False)
assert create(client, not_enabled).json()["error"]["reason"] == "not_enabled"
minor = make_member(age_band="16_17")
assert create(client, minor).json()["error"]["reason"] == "not_adult"
def test_kill_switch_blocks_registration(client, member):
previous = get_setting("nodes_enabled", "0")
set_setting("nodes_enabled", "0")
try:
response = create(client, member)
assert response.status_code == 403
assert response.json()["error"]["reason"] == "kill_switch"
finally:
set_setting("nodes_enabled", previous)
def test_device_cap_is_a_409(client, member):
for _ in range(LIMITS.max_devices_per_user):
assert create(client, member).status_code == 201
assert create(client, member).status_code == 409
def test_unknown_kind_is_a_validation_error(client, member):
assert create(client, member, kind="browser").status_code == 422
def test_rotate_replaces_the_secret_and_is_owner_only(client, member):
created = create(client, member).json()
uid, old = created["device"]["uid"], created["secret"]
other = make_member()
assert (
client.post(f"/nodes/devices/{uid}/rotate", headers=other["headers"]).status_code
== 404
)
rotated = client.post(f"/nodes/devices/{uid}/rotate", headers=member["headers"])
assert rotated.status_code == 200
new = rotated.json()["secret"]
assert new != old
assert find_device_by_secret(old) is None
assert find_device_by_secret(new)["uid"] == uid
def test_delete_is_owner_or_admin(client, member):
uid = create(client, member).json()["device"]["uid"]
other = make_member()
assert (
client.post(f"/nodes/devices/{uid}/delete", headers=other["headers"]).status_code
== 403
)
admin = make_member(role="Admin")
assert (
client.post(f"/nodes/devices/{uid}/delete", headers=admin["headers"]).status_code
== 200
)
assert client.get("/nodes/devices", headers=member["headers"]).json()["devices"] == []
assert (
client.post(f"/nodes/devices/{uid}/delete", headers=member["headers"]).status_code
== 404
)
def test_owner_can_delete_their_own_device(client, member):
uid = create(client, member).json()["device"]["uid"]
response = client.post(f"/nodes/devices/{uid}/delete", headers=member["headers"])
assert response.status_code == 200 and response.json() == {"ok": True}
+67
View File
@@ -0,0 +1,67 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import set_consent
from devplacepy.database.nodes import LIMITS, suspend_device
from devplacepy.services.messaging.tickets import redeem_ticket, redeem_ticket_row
from tests.api.nodes.devices import create
def bearer(secret):
return {"Authorization": f"Bearer {secret}"}
def registered(client, member):
body = create(client, member).json()
return body["device"]["uid"], body["secret"]
def test_no_credentials_or_a_wrong_secret_is_401(client):
assert client.post("/nodes/ticket").status_code == 401
assert client.post("/nodes/ticket", headers=bearer("nope")).status_code == 401
assert client.post("/nodes/ticket", headers=bearer("dpn_unknown")).status_code == 401
def test_a_session_api_key_is_not_a_device_secret(client, member):
assert client.post("/nodes/ticket", headers=member["headers"]).status_code == 401
def test_a_valid_secret_gets_a_short_single_use_ticket(client, member):
device_uid, secret = registered(client, member)
response = client.post("/nodes/ticket", headers=bearer(secret))
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
body = response.json()
assert body["expires_in"] == LIMITS.ticket_ttl_seconds
assert body["ws_path"] == "/nodes/ws"
row = redeem_ticket_row(body["ticket"], "nodes")
assert row["device_uid"] == device_uid and row["user_uid"] == member["uid"]
assert redeem_ticket_row(body["ticket"], "nodes") is None
def test_a_node_ticket_cannot_open_a_chat_socket(client, member):
_, secret = registered(client, member)
ticket = client.post("/nodes/ticket", headers=bearer(secret)).json()["ticket"]
assert redeem_ticket(ticket) is None
assert redeem_ticket_row(ticket, "nodes") is not None
def test_withdrawing_consent_stops_new_tickets(client, member):
_, secret = registered(client, member)
set_consent("user", member["uid"], "bandwidth_sharing", False)
response = client.post("/nodes/ticket", headers=bearer(secret))
assert response.status_code == 403
assert response.json()["error"]["reason"] == "no_consent"
def test_a_suspended_device_gets_no_ticket(client, member):
device_uid, secret = registered(client, member)
suspend_device(device_uid, "test", 30)
response = client.post("/nodes/ticket", headers=bearer(secret))
assert response.status_code == 403
assert response.json()["error"]["reason"] == "suspended"
def test_a_deleted_device_secret_stops_working(client, member):
device_uid, secret = registered(client, member)
client.post(f"/nodes/devices/{device_uid}/delete", headers=member["headers"])
assert client.post("/nodes/ticket", headers=bearer(secret)).status_code == 401
+155
View File
@@ -0,0 +1,155 @@
# retoor <retoor@molodetz.nl>
import json
import pytest
from starlette.websockets import WebSocketDisconnect
from devplacepy.services.manager import service_manager
from devplacepy.services.nodes.spec import (
CLOSE_CAPACITY,
CLOSE_KICKED,
CLOSE_POLICY,
CLOSE_PROTOCOL,
CLOSE_RETRY_OWNER,
SPEC,
)
from tests.api.nodes.devices import create
from tests.api.nodes.ticket import bearer
def hello(**overrides):
frame = {
"type": "hello",
"protocol": SPEC.version,
"client_version": "0.1.0",
"kind": "native",
"platform": "linux/amd64",
"modes": ["tunnel"],
}
frame.update(overrides)
return json.dumps(frame)
def ticket_for(client, member):
body = create(client, member).json()
ticket = client.post("/nodes/ticket", headers=bearer(body["secret"])).json()["ticket"]
return body["device"]["uid"], body["secret"], ticket
def closed_with(connection):
with pytest.raises(WebSocketDisconnect) as info:
connection.receive_text()
return info.value.code
def test_handshake_welcomes_with_the_published_numbers(client, member):
device_uid, _, ticket = ticket_for(client, member)
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
ws.send_text(hello())
welcome = json.loads(ws.receive_text())
assert welcome["type"] == "welcome"
assert welcome["device_uid"] == device_uid
assert welcome["limits"] == SPEC.limits.describe()
assert {item["suffix"] for item in welcome["destinations"]} == {
item.suffix for item in SPEC.destinations
}
assert welcome["settings"]["enabled"] is True
service = service_manager.get_service("nodes")
assert service.registry.get(device_uid) is not None
ws.send_text(json.dumps({"type": "pong"}))
ws.send_text(json.dumps({"type": "usage", "bytes_up": 5, "bytes_down": 7}))
assert service.registry.get(device_uid) is None
def test_a_ticket_works_once(client, member):
_, _, ticket = ticket_for(client, member)
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
ws.send_text(hello())
ws.receive_text()
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as again:
assert closed_with(again) == CLOSE_POLICY
def test_no_ticket_and_a_chat_ticket_are_refused(client, member):
with client.websocket_connect("/nodes/ws") as ws:
assert closed_with(ws) == CLOSE_POLICY
from devplacepy.services.messaging.tickets import issue_ticket
chat_ticket = issue_ticket(member["uid"])
with client.websocket_connect(f"/nodes/ws?ticket={chat_ticket}") as ws:
assert closed_with(ws) == CLOSE_POLICY
def test_a_non_owner_worker_says_retry(client, member):
_, _, ticket = ticket_for(client, member)
service_manager.set_lock_owner(False)
try:
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
assert closed_with(ws) == CLOSE_RETRY_OWNER
finally:
service_manager.set_lock_owner(True)
def test_protocol_violations_close_loudly(client, member):
cases = [
("not json", CLOSE_PROTOCOL),
(json.dumps({"type": "pong"}), CLOSE_PROTOCOL),
(hello(protocol=SPEC.version + 5), CLOSE_POLICY),
(hello(kind="extension"), CLOSE_POLICY),
(hello(modes=["bogus"]), CLOSE_PROTOCOL),
(hello(extra="x"), CLOSE_PROTOCOL),
]
for frame, expected in cases:
_, _, ticket = ticket_for(client, make_fresh(member))
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
ws.send_text(frame)
assert closed_with(ws) == expected, frame
def make_fresh(member):
from tests.api.nodes.conftest import make_member
return make_member()
def test_an_unsolicited_job_result_is_a_protocol_error(client, member):
_, _, ticket = ticket_for(client, member)
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
ws.send_text(hello())
ws.receive_text()
ws.send_text(
json.dumps(
{"type": "job_result", "job_id": "j", "status": 200, "body_b64": ""}
)
)
assert closed_with(ws) == CLOSE_PROTOCOL
def test_a_second_connection_of_the_same_device_replaces_the_first(client, member):
device_uid, secret, first_ticket = ticket_for(client, member)
second_ticket = client.post("/nodes/ticket", headers=bearer(secret)).json()["ticket"]
with client.websocket_connect(f"/nodes/ws?ticket={first_ticket}") as first:
first.send_text(hello())
first.receive_text()
with client.websocket_connect(f"/nodes/ws?ticket={second_ticket}") as second:
second.send_text(hello())
assert json.loads(second.receive_text())["type"] == "welcome"
kicked = json.loads(first.receive_text())
assert kicked == {"type": "kick", "reason": "replaced"}
assert closed_with(first) == CLOSE_KICKED
def test_capacity_is_a_hard_ceiling(client, member):
from devplacepy.database import get_setting, set_setting
service = service_manager.get_service("nodes")
_, _, ticket = ticket_for(client, member)
previous = get_setting("nodes_max_total", "")
set_setting("nodes_max_total", "0")
try:
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
assert closed_with(ws) == CLOSE_CAPACITY
assert service.counters.snapshot()["rejected_capacity"] >= 1
finally:
set_setting("nodes_max_total", previous)
+36
View File
@@ -208,3 +208,39 @@ def test_the_mature_preference_round_trips(app_server):
)
refresh_snapshot()
assert get_table("users").find_one(uid=user["uid"])["mature_opt_in"] in (0, False)
def test_an_agent_cannot_grant_a_member_only_consent_but_can_withdraw_it(app_server):
session, name, user = _member()
agent = {**JSON, "X-Devii-Agent": "1"}
url = f"{BASE_URL}/profile/{name}/consent"
refused = session.post(
url, data={"kind": "bandwidth_sharing", "granted": "1"}, headers=agent
)
assert refused.status_code == 403
refresh_snapshot()
assert consent_granted("user", user["uid"], "bandwidth_sharing") is False
own = session.post(
url, data={"kind": "bandwidth_sharing", "granted": "1"}, headers=JSON
)
assert own.status_code == 200
refresh_snapshot()
assert consent_granted("user", user["uid"], "bandwidth_sharing") is True
withdrawn = session.post(
url, data={"kind": "bandwidth_sharing", "granted": "0"}, headers=agent
)
assert withdrawn.status_code == 200
refresh_snapshot()
assert consent_granted("user", user["uid"], "bandwidth_sharing") is False
def test_an_agent_can_still_grant_an_ordinary_consent(app_server):
session, name, user = _member()
response = session.post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": "1"},
headers={**JSON, "X-Devii-Agent": "1"},
)
assert response.status_code == 200