@@ -154,5 +154,265 @@ page. Every endpoint follows the shared [Conventions & Errors](/docs/conventions
|
||||
"`403` with a `reason` when the device is suspended or its owner no longer passes the consent gate.",
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-me",
|
||||
method="GET",
|
||||
path="/nodes/me",
|
||||
title="Your donation status",
|
||||
summary="Whether you may donate right now (and the reason when not), your settings, your devices and what you have donated.",
|
||||
auth="user",
|
||||
interactive=True,
|
||||
sample_response={
|
||||
"consent": True,
|
||||
"can_donate": True,
|
||||
"reason": "",
|
||||
"message": "",
|
||||
"settings": {"enabled": True, "bandwidth_mb_day": 500, "max_concurrent": 2},
|
||||
"platforms": ["reddit"],
|
||||
"devices": [DEVICE_SAMPLE],
|
||||
"max_devices": 5,
|
||||
"donated": {"lifetime_requests": 12, "lifetime_bytes": 482113, "today_requests": 3},
|
||||
"limits": {"max_donor_mb_per_day": 20480, "max_concurrent_per_node": 2, "default_donor_mb_per_day": 500},
|
||||
},
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-settings",
|
||||
method="POST",
|
||||
path="/nodes/settings",
|
||||
title="Change your donation settings",
|
||||
summary="Update bandwidth, concurrency, active hours, platforms and display. Unsent fields keep their value.",
|
||||
auth="user",
|
||||
ajax=True,
|
||||
encoding="form",
|
||||
params=[
|
||||
field("enabled", "form", "boolean", False, "true", "Switch donation on or off. Switching on needs the `bandwidth_sharing` consent and is refused when sent by the Devii assistant."),
|
||||
field("bandwidth_mb_day", "form", "integer", False, "500", "Daily bandwidth cap in megabytes."),
|
||||
field("max_concurrent", "form", "integer", False, "2", "Simultaneous requests."),
|
||||
field("active_from", "form", "string", False, "08:00", "Start of the active window, HH:MM."),
|
||||
field("active_to", "form", "string", False, "22:00", "End of the active window, HH:MM."),
|
||||
field("timezone", "form", "string", False, "Europe/Amsterdam", "IANA timezone of the window."),
|
||||
field("allowed_platforms", "form", "string", False, "reddit", "Comma separated platforms you are willing to serve."),
|
||||
field("pause_on_battery", "form", "boolean", False, "true", "Stop donating on battery power."),
|
||||
field("public_display", "form", "boolean", False, "false", "Show your name on the donor leaderboard."),
|
||||
],
|
||||
sample_response={"consent": True, "can_donate": True, "settings": {"enabled": True}},
|
||||
notes=["`403` with `reason: no_consent` when switching on without the consent."],
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-stats",
|
||||
method="GET",
|
||||
path="/nodes/stats",
|
||||
title="Community donation totals",
|
||||
summary="Aggregate numbers only: requests relayed, bytes relayed and number of donors. No per-member data.",
|
||||
auth="public",
|
||||
interactive=True,
|
||||
sample_response={"requests": 1200, "bytes": 90311234, "donors": 7},
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-leaderboard",
|
||||
method="GET",
|
||||
path="/nodes/leaderboard",
|
||||
title="Donor leaderboard",
|
||||
summary="The top donors who chose to be shown. Members are not listed unless they switched on public display.",
|
||||
auth="public",
|
||||
interactive=True,
|
||||
sample_response={"donors": [{"username": "alice", "requests": 400, "bytes": 31200000}]},
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-social-search",
|
||||
method="GET",
|
||||
path="/nodes/social/search",
|
||||
title="Social search",
|
||||
summary="Search public developer discussion on Hacker News, Stack Overflow, GitHub, dev.to, Mastodon and, when donors are online, Reddit.",
|
||||
auth="user",
|
||||
interactive=True,
|
||||
params=[
|
||||
field("q", "query", "string", True, "sqlite wal", "What to search for, up to 200 characters."),
|
||||
field("platforms", "query", "string", False, "hackernews,github", "Comma separated subset. Empty means all."),
|
||||
field("limit", "query", "integer", False, "8", "Hits per platform, 1 to 20."),
|
||||
],
|
||||
sample_response={
|
||||
"query": "sqlite wal",
|
||||
"platforms": ["hackernews", "reddit"],
|
||||
"hits": [
|
||||
{
|
||||
"platform": "hackernews",
|
||||
"title": "SQLite WAL mode explained",
|
||||
"url": "https://news.ycombinator.com/item?id=1",
|
||||
"snippet": "",
|
||||
"author": "someone",
|
||||
"score": 120,
|
||||
"comments": 33,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"via": "direct",
|
||||
}
|
||||
],
|
||||
"unavailable": {"reddit": "no_nodes"},
|
||||
},
|
||||
notes=[
|
||||
"Platforms that could not answer are listed under `unavailable` with a reason code; results are never invented for them.",
|
||||
"Counted against a daily per-member quota (`429` when reached). Bluesky and Lobsters are not covered because they block unauthenticated server access.",
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-internal-fetch",
|
||||
method="POST",
|
||||
path="/nodes/internal/fetch",
|
||||
title="Fetch a page through the donor network",
|
||||
summary="Fetch one page from an allowlisted site through a donor node. Meant for agents and services that hold an API key.",
|
||||
auth="user",
|
||||
encoding="form",
|
||||
params=[
|
||||
field("url", "form", "string", True, "https://www.reddit.com/r/programming/.json", "Absolute URL on an allowlisted host."),
|
||||
field("cache", "form", "boolean", False, "true", "Allow a cached copy."),
|
||||
field("timeout", "form", "integer", False, "15", "Seconds to wait, 1 to 60."),
|
||||
],
|
||||
sample_response={
|
||||
"url": "https://www.reddit.com/r/programming/.json",
|
||||
"status": 200,
|
||||
"via": "fetch",
|
||||
"truncated": False,
|
||||
"elapsed_ms": 812,
|
||||
"content_type": "application/json",
|
||||
"text": "{...}",
|
||||
},
|
||||
notes=[
|
||||
"`403` with `reason: host_not_allowed` for any host that is not on the allowlist, `503` with `reason: no_nodes` when no donor is online, `429` when the daily node quota is reached.",
|
||||
"`via` says how the body arrived: `fetch` (relayed by a donor browser, treat as unverified) or `cache`.",
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-client-version",
|
||||
method="GET",
|
||||
path="/nodes/client-version",
|
||||
title="Client versions and checksums",
|
||||
summary="The protocol versions this server speaks and every downloadable client with its SHA-256.",
|
||||
auth="public",
|
||||
interactive=True,
|
||||
sample_response={
|
||||
"version": "0.1.0",
|
||||
"protocol": 1,
|
||||
"min_protocol": 1,
|
||||
"artifacts": [{"name": "devplace-node-linux-amd64", "size": 6291456, "sha256": "ab12..."}],
|
||||
},
|
||||
),
|
||||
endpoint(
|
||||
id="nodes-download",
|
||||
method="GET",
|
||||
path="/nodes/download/{artifact}",
|
||||
title="Download a client",
|
||||
summary="Download one client artifact listed by the client version endpoint. The SHA-256 is also sent as `X-Content-SHA256`.",
|
||||
auth="public",
|
||||
params=[
|
||||
field("artifact", "path", "string", True, "devplace-node-linux-amd64", "Artifact name."),
|
||||
],
|
||||
sample_response="(binary file)",
|
||||
),
|
||||
endpoint(
|
||||
id="admin-nodes",
|
||||
method="GET",
|
||||
path="/admin/nodes",
|
||||
title="Node network overview",
|
||||
summary="Pool, queue, device breakdowns, today's totals, open flags, recent devices, settings and the kill switch.",
|
||||
auth="admin",
|
||||
interactive=True,
|
||||
sample_response={
|
||||
"enabled": True,
|
||||
"pool": {"online": True, "connected": 12, "capacity": 2000, "accepted": 40, "kicked": 3, "jobs_done": 310, "jobs_failed": 4, "open_circuits": 0, "heartbeat": "2026-01-01T00:00:00+00:00"},
|
||||
"queue": {"depth": 2, "max_depth": 500, "queued": 1, "assigned": 1, "done": 30, "failed": 1, "expired": 0},
|
||||
"devices_by_status": {"active": 14, "suspended": 1},
|
||||
"devices_by_kind": {"native": 10, "extension": 5},
|
||||
"devices_by_asn_class": {"unknown": 15},
|
||||
"devices_by_country": {"unknown": 15},
|
||||
"usage_today": {"requests_ok": 120, "requests_failed": 3, "bytes_up": 40000, "bytes_down": 9000000, "cache_hits_served": 0, "integrity_failures": 0},
|
||||
"flags": [{"uid": "FLAG_UID", "kind": "refusal_mismatch", "detail": "", "status": "open", "device_uid": "DEVICE_UID", "device_label": "Home laptop", "user_uid": "USER_UID", "owner_username": "alice", "created_at": "2026-01-01T00:00:00+00:00"}],
|
||||
"devices": [dict(DEVICE_SAMPLE, owner_uid="USER_UID", owner_username="alice", suspend_reason="")],
|
||||
"settings": [{"key": "nodes_max_total", "label": "Maximum connected nodes", "kind": "int", "value": "2000", "default": "2000", "ceiling": 2000}],
|
||||
"limits": {"max_devices_per_user": 5, "max_connected_nodes": 2000},
|
||||
},
|
||||
notes=[
|
||||
"Per-destination circuit state lives in the memory of the worker that holds the sockets and is not readable from other workers; only the number of open circuits is reported.",
|
||||
"`usage_today` is the current UTC day. Lists are capped at 100 rows.",
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-nodes-killswitch",
|
||||
method="POST",
|
||||
path="/admin/nodes/killswitch",
|
||||
title="Switch community donation on or off",
|
||||
summary="The global kill switch. Switching it off disconnects every node within one reconcile tick.",
|
||||
auth="admin",
|
||||
encoding="form",
|
||||
destructive=True,
|
||||
params=[
|
||||
field("enabled", "form", "boolean", False, "false", "True to switch on, false to switch off. Omit to flip the current state."),
|
||||
],
|
||||
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"enabled": False}},
|
||||
notes=["Audited as `node.killswitch`."],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-nodes-settings",
|
||||
method="POST",
|
||||
path="/admin/nodes/settings",
|
||||
title="Update node network settings",
|
||||
summary="Change the caps and quotas. Only the fields you send are changed; values above the ceiling are clamped to it.",
|
||||
auth="admin",
|
||||
encoding="form",
|
||||
params=[
|
||||
field("nodes_max_total", "form", "integer", False, "2000", "Maximum connected nodes, 0 to 2000."),
|
||||
field("nodes_daily_quota_user", "form", "integer", False, "200", "Daily node requests per member, 0 to 10000."),
|
||||
field("nodes_daily_quota_guest", "form", "integer", False, "0", "Daily node requests per guest, 0 to 1000."),
|
||||
field("nodes_cache_ttl_seconds", "form", "integer", False, "600", "Result cache lifetime in seconds, 0 to 600."),
|
||||
field("nodes_daily_social_user", "form", "integer", False, "300", "Daily social searches per member, 0 to 10000."),
|
||||
field("nodes_social_via_nodes", "form", "string", False, "0", "`1` routes social search through nodes, `0` keeps it direct."),
|
||||
],
|
||||
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"changed": ["nodes_max_total"], "clamped": []}},
|
||||
notes=["Audited as `node.admin.settings` with the old and new values."],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-nodes-suspend",
|
||||
method="POST",
|
||||
path="/admin/nodes/devices/{device_uid}/suspend",
|
||||
title="Suspend a donor device",
|
||||
summary="Suspend one device for 1 to 10080 minutes. The device is disconnected on the next tick and cannot reconnect until the period ends.",
|
||||
auth="admin",
|
||||
encoding="form",
|
||||
destructive=True,
|
||||
params=[
|
||||
field("device_uid", "path", "string", True, "DEVICE_UID", "Device uid."),
|
||||
field("minutes", "form", "integer", True, "60", "Suspension length in minutes, 1 to 10080."),
|
||||
field("reason", "form", "string", False, "Repeated refusals", "Reason, up to 500 characters."),
|
||||
],
|
||||
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"uid": "DEVICE_UID", "minutes": 60}},
|
||||
notes=["Refused (audited as `denied`) when the device owner is a more senior administrator. Audited as `node.suspend`."],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-nodes-unsuspend",
|
||||
method="POST",
|
||||
path="/admin/nodes/devices/{device_uid}/unsuspend",
|
||||
title="Lift a device suspension",
|
||||
summary="Return a suspended device to active.",
|
||||
auth="admin",
|
||||
encoding="form",
|
||||
params=[
|
||||
field("device_uid", "path", "string", True, "DEVICE_UID", "Device uid."),
|
||||
],
|
||||
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"uid": "DEVICE_UID"}},
|
||||
notes=["`409` when the device is not suspended. Audited as `node.unsuspend`."],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-nodes-flag-resolve",
|
||||
method="POST",
|
||||
path="/admin/nodes/flags/{flag_uid}/resolve",
|
||||
title="Resolve a node flag",
|
||||
summary="Mark an open integrity or refusal flag as handled.",
|
||||
auth="admin",
|
||||
encoding="form",
|
||||
params=[
|
||||
field("flag_uid", "path", "string", True, "FLAG_UID", "Flag uid."),
|
||||
],
|
||||
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"uid": "FLAG_UID"}},
|
||||
notes=["`409` when the flag is already resolved. Audited as `node.flag.resolve`."],
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user