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 /packages/meshbay-hub/src | |
| 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 'packages/meshbay-hub/src')
21 files changed, 55 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py index dbda197..1cb8bf9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py @@ -213,6 +213,7 @@ async def admin_stats( current_user: User = Depends(require_moderator), db: AsyncSession = Depends(get_db), ): + """Account, group and node counts.""" # Deleted accounts are tombstoned rather than dropped, so that the # connection log stays readable. They are not users any more and must not be # counted as any: a hub whose user count only ever rises is measuring its @@ -244,6 +245,7 @@ async def admin_list_users( offset: int = 0, limit: int = Query(default=50, le=200), ): + """Search and list accounts.""" query = (select(User).where(User.status != "deleted") .order_by(User.created_at.desc())) if q: @@ -279,6 +281,7 @@ async def admin_get_user( current_user: User = Depends(require_moderator), db: AsyncSession = Depends(get_db), ): + """One account, with its group count.""" user = await db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -310,6 +313,7 @@ async def admin_patch_user( current_user: User = Depends(require_moderator), db: AsyncSession = Depends(get_db), ): + """Change an account's status, or its role (admin only).""" user = await db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -473,6 +477,7 @@ async def admin_list_groups( offset: int = 0, limit: int = Query(default=50, le=200), ): + """List groups with their member counts.""" query = ( select( Group, @@ -524,6 +529,7 @@ async def admin_patch_group( current_user: User = Depends(require_moderator), db: AsyncSession = Depends(get_db), ): + """Change a group's status.""" group = await db.get(Group, group_id) if not group: raise HTTPException(status_code=404, detail="Group not found") @@ -624,6 +630,7 @@ async def admin_list_logs( offset: int = 0, limit: int = Query(default=50, le=200), ): + """The connection log, filtered by account and event.""" query = ( select(IPLog, User.username) .outerjoin(User, IPLog.user_id == User.id) diff --git a/packages/meshbay-hub/src/meshbay_hub/api/federation.py b/packages/meshbay-hub/src/meshbay_hub/api/federation.py index 755639e..e737a1c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/federation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/federation.py @@ -190,6 +190,7 @@ async def export_directory( db: AsyncSession = Depends(get_db), authorization: str = Header(...), ): + """This hub's public groups, for a peer hub presenting an MHP token.""" try: await _verify_mhp_token(authorization.removeprefix("Bearer "), db) except Exception as e: @@ -232,6 +233,7 @@ async def receive_directory( authorization: str = Header(...), db: AsyncSession = Depends(get_db), ): + """A peer hub's public groups, pushed with a single-use MHP token.""" try: payload = await _verify_mhp_token( authorization.removeprefix("Bearer "), db, single_use=True) diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py index fc117bf..d58e32c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py @@ -120,6 +120,7 @@ async def accept_invitation( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """Accept an invitation: the account becomes a member of the group.""" inv = await db.get(GroupInvitation, (group_id, current_user.id)) group = await db.get(Group, group_id) if inv is None or group is None or group.status != "active": @@ -141,6 +142,7 @@ async def decline_invitation( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """Decline an invitation to a group.""" inv = await db.get(GroupInvitation, (group_id, current_user.id)) if inv is None: raise HTTPException(status_code=404, detail="No such invitation") @@ -281,6 +283,7 @@ async def group_members( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): + """A group's members, for its members only.""" group = await db.get(Group, group_id) if not group: raise HTTPException(status_code=404, detail="Group not found") @@ -319,6 +322,7 @@ async def join_group( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """Join an open group.""" group = await db.get(Group, group_id) if not group: raise HTTPException(status_code=404, detail="Group not found") @@ -421,6 +425,7 @@ async def create_group( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """Create a group, owned by the caller. No node hosts it yet.""" # Being listed and being open are one question, not two. # # A public group that admits nobody is a contradiction: it is in the @@ -643,6 +648,7 @@ async def add_group_member( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): + """Add an account to a group the caller owns. Also called by the owner's node.""" # `get_current_user`, not `require_user_scope`: the node calls this after a # CLI `member invite` so the group becomes visible in the invitee's SPA # (commit 0443cf8). The node authenticates with a node-scoped token, and the @@ -713,6 +719,7 @@ async def delete_group( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """Delete a group. Its owner only.""" group = await db.get(Group, group_id) if not group: raise HTTPException(status_code=404, detail="Group not found") @@ -827,6 +834,7 @@ async def list_hosts( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """The nodes that host a group or asked to, for its owner.""" from meshbay_hub.api.revocation import is_node_connected await _owned(db, group_id, current_user) rows = (await db.execute( diff --git a/packages/meshbay-hub/src/meshbay_hub/api/health.py b/packages/meshbay-hub/src/meshbay_hub/api/health.py index 516856b..abb1b53 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/health.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/health.py @@ -13,6 +13,7 @@ router = APIRouter(tags=["health"]) @router.get("/v1/health") async def health(db: AsyncSession = Depends(get_db)): + """Liveness: database reachable, version, connected nodes.""" await db.execute(text("SELECT 1")) return { "status": "ok", diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py index 2a3efaf..d1246a3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py @@ -22,6 +22,7 @@ def set_config(cfg: HubConfig) -> None: @router.get("/info") async def hub_info(db: AsyncSession = Depends(get_db)): + """Versions and the instance policy a client needs before signing in.""" engine = get_engine() return { "hub_version": __version__, diff --git a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py index 038f310..ecca4df 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py @@ -244,6 +244,7 @@ async def admin_list_blocklist( db: AsyncSession = Depends(get_db), limit: int = 500, ): + """The content blocklist.""" result = await db.execute( select(ContentBlocklist) .order_by(ContentBlocklist.added_at.desc()) @@ -269,6 +270,7 @@ async def admin_add_blocklist( current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): + """Add a content hash (BLAKE3) to the blocklist.""" if not _is_hash(body.content_hash): raise HTTPException(status_code=422, detail="content_hash must be 64 hex chars (blake3)") existing = await db.get(ContentBlocklist, body.content_hash) @@ -291,6 +293,7 @@ async def admin_remove_blocklist( current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): + """Remove a content hash from the blocklist.""" entry = await db.get(ContentBlocklist, content_hash) if not entry: raise HTTPException(status_code=404, detail="Hash not in blocklist") @@ -344,6 +347,7 @@ async def admin_block_reported( current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): + """Block reported content and close its reports.""" await _decide(db, content_hash, "blocked", current_user.username) reasons = Counter((await db.execute(select(ContentReport.reason).where( ContentReport.content_hash == content_hash))).scalars().all()) @@ -363,6 +367,7 @@ async def admin_dismiss_reported( current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): + """Dismiss the reports on a piece of content.""" await _decide(db, content_hash, "dismissed", current_user.username) await db.commit() return {"status": "dismissed", "hash": content_hash} diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py index decd20e..b158621 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py @@ -250,6 +250,7 @@ async def get_node( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): + """A node's public record: owner, key, endpoint hint.""" node = await db.get(Node, node_id) if not node: raise HTTPException(status_code=404, detail="Node not found") diff --git a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py index d96ec18..e4bac2e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py @@ -43,6 +43,7 @@ async def list_notifications( offset: int = Query(default=0, ge=0), unread_only: bool = False, ): + """The account's notifications, newest first.""" query = select(Notification).where(Notification.user_id == current_user.id) if unread_only: query = query.where(Notification.read == False) # noqa: E712 diff --git a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py index 5a33d77..2499374 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py @@ -438,7 +438,8 @@ async def _authorize_node_ws(token: str, claimed_id: str, claimed_groups) -> tup @router.websocket("/v1/nodes/ws") async def node_websocket(ws: WebSocket): """ - Persistent WebSocket connection for nodes. + Persistent WebSocket connection for nodes, authenticated by the node's token in the + first message. Finding C2: this used to take `node_id` and `group_ids` straight from the client's first message, with no check that the authenticated user owned that diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index 9326bfb..795a902 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -183,6 +183,7 @@ async def register( request: Request, db: AsyncSession = Depends(get_db), ): + """Create an account. It stays inactive until its e-mail address is verified.""" eh = hash_email_blind(body.email) # Unique regardless of case: invitations and member management name people @@ -485,6 +486,10 @@ async def login( request: Request, db: AsyncSession = Depends(get_db), ): + """ + Sign in with the auth key derived from the passphrase. Returns the session and the bundle + pepper. + """ ip = client_ip(request) if not body.auth_key and not body.password: @@ -662,6 +667,7 @@ async def list_devices( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): + """The account's registered devices.""" result = await db.execute( select(UserDevice).where(UserDevice.user_id == current_user.id) .order_by(UserDevice.created_at)) @@ -777,6 +783,7 @@ async def token_refresh( request: Request, db: AsyncSession = Depends(get_db), ): + """Exchange a refresh token for a new session. Reusing a spent one revokes the whole family.""" rt_hash = hash_refresh_token(body.refresh_token) result = await db.execute( select(RefreshToken).where(RefreshToken.token_hash == rt_hash)) @@ -840,6 +847,7 @@ async def get_current_user_info( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): + """The signed-in account: id, name, e-mail, role, status.""" email = "" try: email = decrypt_email(current_user.email) if current_user.email else "" @@ -1144,6 +1152,7 @@ async def change_password( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """Change the passphrase, proving the current one.""" await _take_login_attempt(db, _session_counter(current_user)) if not await verify_password_off_loop(body.old_auth_key, current_user.pw_hash, current_user.pw_salt, current_user.pw_version): @@ -1231,6 +1240,7 @@ async def password_reset_request( request: Request, db: AsyncSession = Depends(get_db), ): + """Send a reset code by e-mail, when the username and the address match.""" if _cfg and _cfg.captcha.enabled: await _verify_captcha_or_raise(body.captcha_token, request) @@ -1310,6 +1320,7 @@ async def password_reset( request: Request, db: AsyncSession = Depends(get_db), ): + """Set a new passphrase with the code received by e-mail.""" now = datetime.now(UTC) result = await db.execute(select(User).where(User.username == body.username)) user = result.scalar_one_or_none() @@ -1407,6 +1418,7 @@ async def get_preferences( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): + """The account's stored interface preferences.""" result = await db.execute( select(UserPreference).where(UserPreference.user_id == current_user.id)) prefs = {p.key: p.value for p in result.scalars().all()} @@ -1424,6 +1436,7 @@ async def set_preference( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """Store one interface preference.""" if not _valid_pref_key(key): raise HTTPException(status_code=400, detail=f"Unknown preference key: {key[:80]}") if len(body.value) > MAX_PREFERENCE_VALUE: @@ -1454,6 +1467,7 @@ async def delete_preference( current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + """Remove one interface preference.""" result = await db.execute( select(UserPreference).where( UserPreference.user_id == current_user.id, @@ -1624,6 +1638,7 @@ async def get_user_pubkeys( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): + """Resolve a username to its account id, and its node's linking key. Not a key directory.""" result = await db.execute(select(User).where(User.username == username)) target = result.scalar_one_or_none() if not target: diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js index cb95816..c12db72 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js @@ -276,7 +276,8 @@ const WELCOME_DOCS = [ ['welcome.docs_userguide', `${REPO}docs/USERGUIDE.md`]]], ['gear', 'welcome.docs_devel', [ ['welcome.docs_design', `${REPO}docs/MESHBAY_DESIGN.md`], - ['welcome.docs_protocol', `${REPO}docs/MESHBAY_NODE_PROTOCOL.md`]]], + ['welcome.docs_protocol', `${REPO}docs/MESHBAY_NODE_PROTOCOL.md`], + ['welcome.docs_api', `${REPO}docs/MESHBAY_HTTP_API.md`]]], ]; // Under the sign-in form rather than at the foot of the text: on a desktop the diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index bb54c0d..0221c9a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -123,6 +123,7 @@ export default { 'welcome.docs_devel': 'Entwicklerdoku', 'welcome.docs_design': 'Architektur', 'welcome.docs_protocol': 'Protokoll', + 'welcome.docs_api': 'API', 'welcome.download': 'Herunterladen (Beta)', 'welcome.legal': 'Rechtliche Hinweise', 'register.err_mismatch': 'Die Passwörter stimmen nicht überein', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 1bed529..4ee28cd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -126,6 +126,7 @@ export default { 'welcome.docs_devel': 'Developer docs', 'welcome.docs_design': 'Design', 'welcome.docs_protocol': 'Protocol', + 'welcome.docs_api': 'API', 'welcome.download': 'Download (beta)', 'welcome.legal': 'Legal information', 'register.err_mismatch': 'Passwords do not match', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 936d8b6..f9f1497 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -122,6 +122,7 @@ export default { 'welcome.docs_devel': 'Docs de desarrollo', 'welcome.docs_design': 'Diseño', 'welcome.docs_protocol': 'Protocolo', + 'welcome.docs_api': 'API', 'welcome.download': 'Descargar (beta)', 'welcome.legal': 'Información legal', 'register.err_mismatch': 'Las contraseñas no coinciden', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 811db8d..6561ace 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -122,6 +122,7 @@ export default { 'welcome.docs_devel': 'Docs développeur', 'welcome.docs_design': 'Conception', 'welcome.docs_protocol': 'Protocole', + 'welcome.docs_api': 'API', 'welcome.download': 'Télécharger (bêta)', 'welcome.legal': 'Informations légales', 'register.err_mismatch': 'Les mots de passe ne correspondent pas', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 63bff86..052d02f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -123,6 +123,7 @@ export default { 'welcome.docs_devel': 'Documentazione tecnica', 'welcome.docs_design': 'Architettura', 'welcome.docs_protocol': 'Protocollo', + 'welcome.docs_api': 'API', 'welcome.download': 'Scarica (beta)', 'welcome.legal': 'Note legali', 'register.err_mismatch': 'Le password non coincidono', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index d9677c7..0c91c56 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -123,6 +123,7 @@ export default { 'welcome.docs_devel': '開発者向け', 'welcome.docs_design': '設計', 'welcome.docs_protocol': 'プロトコル', + 'welcome.docs_api': 'API', 'welcome.download': 'ダウンロード(ベータ版)', 'welcome.legal': '法的情報', 'register.err_mismatch': 'パスワードが一致しません', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 2ddb872..cccb3cb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -123,6 +123,7 @@ export default { 'welcome.docs_devel': 'Ontwikkelaarsdocs', 'welcome.docs_design': 'Ontwerp', 'welcome.docs_protocol': 'Protocol', + 'welcome.docs_api': 'API', 'welcome.download': 'Downloaden (bèta)', 'welcome.legal': 'Juridische informatie', 'register.err_mismatch': 'De wachtwoorden komen niet overeen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 504dcd9..9937af6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -126,6 +126,7 @@ export default { 'welcome.docs_devel': 'Dla programistów', 'welcome.docs_design': 'Architektura', 'welcome.docs_protocol': 'Protokół', + 'welcome.docs_api': 'API', 'welcome.download': 'Pobierz (beta)', 'welcome.legal': 'Informacje prawne', 'register.err_mismatch': 'Hasła nie są zgodne', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index df1c140..0f6b331 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -124,6 +124,7 @@ export default { 'welcome.docs_devel': 'Docs de desenvolvimento', 'welcome.docs_design': 'Arquitetura', 'welcome.docs_protocol': 'Protocolo', + 'welcome.docs_api': 'API', 'welcome.download': 'Baixar (beta)', 'welcome.legal': 'Informações legais', 'register.err_mismatch': 'As senhas não coincidem', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 8e72359..ebefc71 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -123,6 +123,7 @@ export default { 'welcome.docs_devel': '开发文档', 'welcome.docs_design': '设计', 'welcome.docs_protocol': '协议', + 'welcome.docs_api': 'API', 'welcome.download': '下载(测试版)', 'welcome.legal': '法律信息', 'register.err_mismatch': '两次输入的密码不一致', |