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:
@@ -1,6 +1,20 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from .._shared import endpoint
|
||||
from .._shared import endpoint, field
|
||||
|
||||
DEVICE_SAMPLE = {
|
||||
"uid": "DEVICE_UID",
|
||||
"label": "Home laptop",
|
||||
"kind": "native",
|
||||
"platform": "linux/amd64",
|
||||
"client_version": "0.1.0",
|
||||
"status": "active",
|
||||
"suspended_until": None,
|
||||
"country": "NL",
|
||||
"asn_class": "residential",
|
||||
"last_seen_at": "2026-01-01T00:00:00+00:00",
|
||||
"created_at": "2026-01-01T00:00:00+00:00",
|
||||
}
|
||||
|
||||
GROUP = {
|
||||
"slug": "nodes",
|
||||
@@ -65,5 +79,80 @@ page. Every endpoint follows the shared [Conventions & Errors](/docs/conventions
|
||||
"Clients and the server read the same numbers from here; a contract test fails when they drift.",
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-devices-list",
|
||||
method="GET",
|
||||
path="/nodes/devices",
|
||||
title="List your devices",
|
||||
summary="The devices you registered, with their status and when they last connected. Secrets are never listed.",
|
||||
auth="user",
|
||||
interactive=True,
|
||||
sample_response={"devices": [DEVICE_SAMPLE], "max_devices": 5},
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-devices-create",
|
||||
method="POST",
|
||||
path="/nodes/devices",
|
||||
title="Register a device",
|
||||
summary="Register one client install. The device secret is returned once and cannot be read again.",
|
||||
auth="user",
|
||||
ajax=True,
|
||||
encoding="form",
|
||||
params=[
|
||||
field("label", "form", "string", False, "Home laptop", "A name only you see, up to 60 characters."),
|
||||
field("kind", "form", "string", False, "native", "`native`, `extension` or `container`.", options=["native", "extension", "container"]),
|
||||
field("platform", "form", "string", False, "linux/amd64", "Operating system and architecture of the client."),
|
||||
field("client_version", "form", "string", False, "0.1.0", "Version of the client."),
|
||||
],
|
||||
sample_response={"device": DEVICE_SAMPLE, "secret": "dpn_EXAMPLE_SECRET_SHOWN_ONCE"},
|
||||
notes=[
|
||||
"Refused with `403` and a `reason` (`kill_switch`, `no_consent`, `not_enabled`, `not_adult`, `deletion_pending`, `suspended`, `account_inactive`) unless donation is switched on, you are an adult, you have given the `bandwidth_sharing` consent and enabled donation for yourself.",
|
||||
"Refused with `409` when you already have the maximum number of devices.",
|
||||
"The response is `201` and is never cached.",
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-devices-rotate",
|
||||
method="POST",
|
||||
path="/nodes/devices/{device_uid}/rotate",
|
||||
title="Rotate a device secret",
|
||||
summary="Issue a new secret for one of your devices. The old secret stops working immediately.",
|
||||
auth="user",
|
||||
ajax=True,
|
||||
params=[
|
||||
field("device_uid", "path", "string", True, "DEVICE_UID", "Device uid."),
|
||||
],
|
||||
sample_response={"uid": "DEVICE_UID", "secret": "dpn_EXAMPLE_SECRET_SHOWN_ONCE"},
|
||||
notes=["`404` for a device that is not yours."],
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-devices-delete",
|
||||
method="POST",
|
||||
path="/nodes/devices/{device_uid}/delete",
|
||||
title="Delete a device",
|
||||
summary="Remove a device. A connected node is disconnected on the next service tick. An administrator may remove any device.",
|
||||
auth="user",
|
||||
ajax=True,
|
||||
destructive=True,
|
||||
params=[
|
||||
field("device_uid", "path", "string", True, "DEVICE_UID", "Device uid."),
|
||||
],
|
||||
sample_response={"ok": True},
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-ticket",
|
||||
method="POST",
|
||||
path="/nodes/ticket",
|
||||
title="Exchange a device secret for a socket ticket",
|
||||
summary="The node client calls this with its device secret to get a single-use ticket for `WS /nodes/ws?ticket=`.",
|
||||
auth="user",
|
||||
interactive=False,
|
||||
sample_response={"ticket": "TICKET", "expires_in": 30, "ws_path": "/nodes/ws"},
|
||||
notes=[
|
||||
"Authenticated by `Authorization: Bearer dpn_...` with the device secret, not by a session. Anything else is `401`.",
|
||||
"The ticket is valid for 30 seconds and can be redeemed once. The socket then speaks the frames published in the [protocol specification](/docs/nodes-protocol.html).",
|
||||
"`403` with a `reason` when the device is suspended or its owner no longer passes the consent gate.",
|
||||
],
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user