diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-05 10:36:18 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-05 10:36:18 +0200 |
| commit | b8671635cd891068afee81fde05bed880124ec85 (patch) | |
| tree | 8e5ba99cd13558f88d2a31eb9d0f4d937128d255 /docs/generate_http_api.py | |
| parent | 41d015137d6d9c097e462cd0662e855c3253d001 (diff) | |
| download | meshbay-b8671635cd891068afee81fde05bed880124ec85.tar.gz | |
docs: generate an HTTP API listing for the hub and the node control API
docs/MESHBAY_HTTP_API.md lists every route of the hub (by domain, with the
authentication each requires) and of the node's loopback control API. It is
written by docs/generate_http_api.py from the routes and their docstrings;
test_http_api_doc.py fails when the file drifts from the code or when a
route has no docstring, so a new route must say what it does.
79 routes had no docstring and get a one-line description; a few whose first
line did not describe the route get a summary line.
The login page's developer docs gain an API link next to Design and
Protocol, in every language. README, MESHBAY_DESIGN.md (§0.1, §6.7, §7) and
CLAUDE.md point to the listing; README also points to examples/.
The examples scripts with a shebang become executable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/generate_http_api.py')
| -rw-r--r-- | docs/generate_http_api.py | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/docs/generate_http_api.py b/docs/generate_http_api.py new file mode 100644 index 0000000..d064519 --- /dev/null +++ b/docs/generate_http_api.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +Write docs/MESHBAY_HTTP_API.md from the routes of the hub and of the node's +control API. + + python docs/generate_http_api.py + +Generated rather than written: a list of a hundred routes kept by hand is wrong +by the next one added. `test_http_api_doc.py` fails when the file and the code +disagree, and when a route has no docstring to describe it. +""" + +import inspect +import re +import sys +from pathlib import Path + +from fastapi.routing import APIRoute, APIWebSocketRoute + +OUT = Path(__file__).resolve().parent / "MESHBAY_HTTP_API.md" + +# The hub's routers, by module, in the order the hub includes them. +SECTIONS = { + "hub": "Instance", + "users": "Accounts", + "nodes": "Nodes", + "groups": "Groups", + "invite_links": "Invitation links", + "revocation": "Revocation and the node socket", + "moderation": "Moderation", + "federation": "Federation (MHP)", + "health": "Health", + "signaling": "Signaling", + "admin": "Administration", + "notifications": "Notifications", +} + +# Strongest first: a route is labelled by the first dependency it carries. +AUTH = [ + ("require_admin", "admin"), + ("require_moderator", "moderator"), + ("require_node_scope", "node"), + ("require_user_scope", "user"), + ("get_current_user", "user or node"), + ("_decode_token", "token"), + ("_federation_open", "peer hub"), +] + +AUTH_LEGEND = """\ +| Auth | What the request carries | +|---|---| +| none | No session. Any credential is in the request itself, as the route says | +| user | `Authorization: Bearer`, a person's session. A node's token is refused | +| user or node | A person's session, or a node daemon's token from `/v1/nodes/auth` | +| node | A node daemon's token only | +| moderator | A person's session, for an account with the moderator or admin role | +| admin | A person's session, for an account with the admin role | +| peer hub | Another hub's MHP token. Every route answers 503 unless federation is enabled | +""" + +HEADER = """\ +# MeshBay HTTP API + +> **Generated** by `docs/generate_http_api.py` from the routes themselves. Do not +> edit this file: change the route's docstring and run the script again. A test +> fails when the two disagree. + +MeshBay has two HTTP APIs: the **hub's**, for accounts, groups and signaling, and +the **node's control API**, which only its own machine reaches. Files, the index +and chat do not use either: they travel between a client and a node over MNP +(`MESHBAY_NODE_PROTOCOL.md`). Why each API is shaped as it is, who may call +what and what the hub must never see are in `MESHBAY_DESIGN.md`; this file lists +what exists. + +`examples/` has small Python programs that use both. +""" + +HUB_INTRO = """\ + +## Hub + +Under the hub's address, `https://meshbay.org` on the reference deployment. The +hub also serves the web application at `/` and `/app/`, which are not listed. + +""" + +NODE_INTRO = """\ +## Node control API + +`http://127.0.0.1:<ui_port>`, port 18000 unless `ui_port` in `node.toml` says +otherwise, and never on another address. Every request carries the token the +daemon writes to `<data_dir>/ui-token` (`~/.local/share/meshbay/ui-token` on +Linux), as the `X-MeshBay-Token` header or the `t` query parameter. The daemon +draws a new one at each start and deletes the file when it stops. + +| Method | Path | What | +|---|---|---| +""" + + +def flatten(routes): + for r in routes: + if hasattr(r, "original_router"): + yield from flatten(r.original_router.routes) + elif isinstance(r, (APIRoute, APIWebSocketRoute)): + yield r + + +def summary(route) -> str: + """The docstring's first sentence.""" + doc = inspect.getdoc(route.endpoint) or "" + first = " ".join(doc.split("\n\n")[0].split()) + return re.split(r"(?<=[.!?])\s+(?=[A-Z`])", first)[0].replace("|", "\\|") + + +def method(route) -> str: + if isinstance(route, APIWebSocketRoute): + return "WS" + return ", ".join(sorted(route.methods - {"HEAD"})) + + +def auth(route) -> str: + names = set() + + def walk(dependant): + for d in dependant.dependencies: + if d.call is not None: + names.add(getattr(d.call, "__name__", "")) + walk(d) + + walk(route.dependant) + return next((label for name, label in AUTH if name in names), "none") + + +def hub_routes() -> list: + from meshbay_hub.app import create_app + return [r for r in flatten(create_app().routes) + if r.endpoint.__module__.rsplit(".", 1)[-1] != "webapp"] + + +def node_routes() -> list: + from meshbay_node.ui.app import create_ui_app + return list(flatten(create_ui_app({}).routes)) + + +def render() -> str: + out = [HEADER, HUB_INTRO, AUTH_LEGEND] + by_module: dict[str, list] = {} + for r in hub_routes(): + by_module.setdefault(r.endpoint.__module__.rsplit(".", 1)[-1], []).append(r) + for module, routes in by_module.items(): + out.append(f"\n### {SECTIONS.get(module, module.replace('_', ' ').capitalize())}\n\n") + out.append("| Method | Path | Auth | What |\n|---|---|---|---|\n") + for r in routes: + out.append(f"| {method(r)} | `{r.path}` | {auth(r)} | {summary(r)} |\n") + out.append("\n" + NODE_INTRO) + for r in node_routes(): + out.append(f"| {method(r)} | `{r.path}` | {summary(r)} |\n") + return "".join(out) + + +if __name__ == "__main__": + OUT.write_text(render(), encoding="utf-8") + print(f"wrote {OUT}", file=sys.stderr) |