Skip to content

Latest commit

 

History

History
270 lines (161 loc) · 15 KB

File metadata and controls

270 lines (161 loc) · 15 KB

API Protocol

Canonical reference for the Anemoia API: WebSocket JSON protocol and REST auth. Used by the Preact client and any future native clients.

Operator configuration for registration modes, SMTP, rate limits, and reverse-proxy trust is documented in .env.example (production) and .env.dev.example (development). Gmail / app passwords: docs/smtp.md.


WebSocket

  • URL: /ws
  • Auth: Token in query string: /ws?token=<session_token>. The token comes from POST /api/auth/login or POST /api/auth/register when a web session is created (see Registration modes — pending flows do not return a token until the user is active and logs in). The IRC bridge password is sent as an HTTP-only cookie on the upgrade request; the cookie is set on login/register when a web session is created. Both a valid session token and the credential cookie are required to connect.
  • Format: JSON text frames. One JSON object per frame.
  • Close codes:
    • 4401 — Unauthorized: missing or invalid token, missing credential cookie, or user not allowed to use the bridge (client should clear session and redirect to login).
    • 4000 — Server error (e.g. session service unavailable).

Message types

Client → Server

Type Payload Meaning
message target, text Send a chat message to channel or DM (target = channel name or nick).
join channel Join a channel.
part channel, reason? Part a channel.
typing target Typing indicator (may be ignored).
history channel, before?, limit? Request scrollback; limit default 50, max 200.

Server → Client

Type Payload Meaning
status connected, reason? Connection state.
message from, target, text, time, id? Chat message (time ISO). ACTION: text \u0001ACTION ...\u0001.
join nick, channel User joined channel.
part nick, channel, reason? User left channel.
userlist channel, users users = [{ nick, away }].
topic channel, text, set_by? Channel topic.
history channel, messages Scrollback response.
error message Error string.
channels_changed (none) Refetch GET /api/channels and refresh the sidebar.

Registration modes

Controlled by REGISTRATION_MODE: open (default), email_code, or approval.

Mode On register Session / cookie Ergo account
open User is active immediately Token + credential cookie Created immediately (SAREGISTER)
email_code User pending until email verified No token or cookie Created after successful verify-email
approval User pending until admin approves No token or cookie Created after admin approve
  • email_code requires SMTP and PENDING_REGISTRATION_KEY at API startup; the process exits if either is misconfigured.
  • approval requires PENDING_REGISTRATION_KEY at startup; SMTP is optional (used only to email admins when new registrations arrive, if configured).

REST auth

Send credentials: 'same-origin' on register/login so the credential cookie is set when applicable. Use the returned token as Authorization: Bearer <token> and in /ws?token=... when a token is returned.

POST /api/auth/register

Body: { nick, email, password }nick must be a valid IRC nick (same rules as login); display_name in the DB is stored as a copy of nick for schema compatibility.

open mode — success 201: { userId, nick, token, role } (and credential cookie).

email_code — success 201: { userId, nick, pendingVerification: true } — no token, no cookie.

approval — success 201: { userId, nick, pendingApproval: true } — no token, no cookie.

Errors: 400 validation (including invalid nick), 409 duplicate email or nick unavailable (already taken in Ergo or the API DB), 500 if Ergo account creation fails (open).

Rate-limited per IP (see env RATE_LIMIT_REGISTER_*). With TRUST_PROXY=1, the API trusts X-Forwarded-For / X-Real-IP for the client IP used by rate limiting.

POST /api/auth/login

Body: { nick, password, logOutOtherSessions? }

Success 200: { userId, nick, token, role } + credential cookie. role is admin or user.

Errors:

  • 401 — wrong nick/password.
  • 403 — correct password but account not active: { code: 'pending_verification' | 'pending_approval', message }.

POST /api/auth/verify-email

Body: { email, code } (six-digit code; normalize whitespace server-side).

HTTP Body
200 { verified: true } — user is now active; Ergo account created. Client should then POST /api/auth/login with nick + password.
400 { code: 'verification_failed', message } — generic (wrong code, unknown email, expired, not pending); do not distinguish cases (enumeration-safe).
409 { code: 'nick_unavailable', message } — nick already registered in Ergo before SAREGISTER.
500 Server/oper failure; user stays pending.

Rate-limited: RATE_LIMIT_VERIFY_EMAIL_*. No 401 on this route.

POST /api/auth/resend-code

Body: { email }

Always 200 with { ok: true } whether the email exists, is pending, or not (enumeration-safe). If the user exists and status is pending_email, a new code is emailed and any previous code is invalidated.

Rate-limited: RATE_LIMIT_RESEND_CODE_*.

GET /api/auth/me

Header: Authorization: Bearer <token>

  • 200: { userId, nick, email, display_name, avatar_url, bio, role } — user is active and session valid; role is admin or user. avatar_url and bio may be null. display_name is the stored label (defaults to nick when unset).
  • 401 — missing/invalid/expired token.
  • 403: { code: 'account_inactive', message } — token matches a session but the user is not active (e.g. pending approval); client should clear UI state / show appropriate screen.

PATCH /api/auth/profile

Header: Authorization: Bearer <token>
Body: JSON object with any of display_name (string), avatar_url (string or null), bio (string or null). Omitted keys are left unchanged. display_name is trimmed; empty string clears to the default (server may store nick). avatar_url must be a valid URL per server validation (strict-url-sanitise). bio is trimmed; max 500 Unicode codepoints; markdown allowed; no raw HTML.

Success 200: Same shape as GET /api/auth/me.

Errors: 400 validation (invalid URL, bio too long, etc.). 401 invalid session.

POST /api/auth/forgot-password

Body: { email } — trimmed; match is case-insensitive on stored email.

Success 200: { ok: true } — if an active account exists for that email, sends a one-hour reset link; if not, same response (enumeration-safe).

Errors:

  • 503 { code: 'smtp_not_configured', message } — SMTP env vars not set; password reset email cannot be sent.
  • 503 { code: 'public_url_not_configured', message } — cannot build the link (set PUBLIC_URL or REGISTRATION_URL, or ensure reverse proxy sends Host / X-Forwarded-*).
  • 500 — email send failure after creating a token (token is rolled back).

Rate-limited: RATE_LIMIT_FORGOT_PASSWORD_*. Reset links point to {PUBLIC or REGISTRATION base}/reset-password?token=... (see server getPublicWebBaseUrl).

POST /api/auth/reset-password

Body: { token, newPassword }token from the email link query string; newPassword min 8 characters.

Success 204. Updates DB and Ergo (PASSWD via oper when oper client is configured); deletes all sessions for that user and clears the credential cookie.

Errors: 400 { code: 'invalid_token', message } — unknown, expired, or already-used token.

PATCH /api/auth/password

Header: Authorization: Bearer <token>
Body: { currentPassword, newPassword }

Success 204. Updates DB and Ergo (PASSWD via oper); refreshes credential cookie when applicable.

POST /api/auth/logout

Header: Authorization: Bearer <token> and/or body: { token }

204. Clears credential cookie and deletes the session.


Admin: pending registrations (approval mode)

Requires Bearer token and admin role.

Method Path Success Notes
GET /api/admin/pending-registrations 200 — list of { id, nick, email, created_at } Users with pending_approval only
POST /api/admin/pending-registrations/:userId/approve 204 Activates user, SAREGISTER on Ergo; emails the user Your account has been approved when SMTP is configured (failure to send is logged only; response is still 204); 409 { code: 'nick_unavailable', message } if nick taken
POST /api/admin/pending-registrations/:userId/reject 204 Deletes pending user (and verification rows); frees nick

User directory

Method Path Success Notes
GET /api/admin/users 200 — array of { id, nick, email, role, status, created_at } Query: limit (default 50, max 200), search (optional substring on nick, case-insensitive). Excludes pending_approval (use pending-registrations).

MOTD (admin)

Method Path Success Notes
GET /api/admin/motd 200 { text } Plain text from ERGO_MOTD_PATH or repo ergo/ergo.motd relative to cwd
PATCH /api/admin/motd 200 { text } Body { text }; writes file then REHASH via oper when available; 503 if save succeeded but reload failed

Channels (admin list & members)

Method Path Success Notes
GET /api/admin/channels 200 — array of { name, topic, created_at, description, ops_count, member_count } description mirrors topic for v1; member_count is null (not tracked in DB); ops_count from channel_ops
GET /api/admin/channels/:name/members 200 { members: [{ userId, nick, access }] } access: op or super_op (from Anemoia DB only; not full IRC NAMES)

GET /api/channels

Public list for the client sidebar.

  • 200: JSON array of { name, topic }, e.g. [{ "name": "#general", "topic": "" }].

Public community & profiles

These routes do not require a session (unless noted). Used for the chat header, display-name hydration, and profile popovers.

GET /api/server/info

  • 200: { name, channels, userCount }name from COMMUNITY_NAME (default Anemoia). channels mirrors the sidebar shape ({ name, topic }[]). userCount is a best-effort count from the IRC bridge health check when available, else 0.

POST /api/users/lookup

Body: { "nicks": string[] } — deduped server-side; max 50 nicks per request.

Success 200: { users: [{ nick, display_name, avatar_url }] } — only active users present in the DB; omitted nicks are not listed.

Rate limit: 120 requests per IP per minute (see server config).

GET /api/users/profile/:nick

Canonical URL for the web client — lives under /api, so it is reverse-proxied to the API in development (Vite) and production (Caddy). Same JSON as below.

Success 200: { nick, display_name, avatar_url, bio } for an active user (case-insensitive nick in the path). bio may be null. No email.

404 — unknown or inactive user.

GET /~:nick

Same JSON as GET /api/users/profile/:nick, for direct hits against the API process (e.g. curl to the API port). Do not use this path from the browser on the same origin as the SPA: the static server’s SPA fallback will serve index.html instead of JSON unless you add a dedicated reverse-proxy rule.


Chat client: markdown → IRC (web composer)

The web client converts inline markdown (remark-gfm subset) to mIRC-style formatting bytes before sending on the WebSocket, so traditional IRC clients see bold/italic/monospace and plain URLs. Block features such as tables are rejected in the composer. URLs are validated with strict-url-sanitise; unsafe URLs are replaced with a placeholder token on the wire. Incoming messages are parsed for the same formatting codes and rendered in the web UI (links and images after sanitisation).


POST /api/uploads

Authenticated upload for chat attachments. Files are stored on disk (UPLOAD_DIR); the reverse proxy serves them at /uploads/<filename> (see deployment.md).

Header: Authorization: Bearer <token>

Body: multipart/form-data with one file part (field name file).

Success 201: { "url": "https://<host>/uploads/<id>.<ext>" }url uses PUBLIC_URL if set (no trailing slash), otherwise https://$DOMAIN, otherwise the API’s own origin in dev.

Errors: 401 missing/invalid session, 400 no file in body, 413 over size limit (UPLOAD_MAX_BYTES, default 5 MiB), 415 unsupported MIME type.

Allowed MIME types (v1): image/jpeg, image/png, image/gif, image/webp, image/svg+xml, application/pdf.


Cryptographic notes (server-side)

  • IRC_CREDENTIAL_KEY: Base64; decodes to ≥32 bytes; first 32 bytes encrypt the IRC password in the HTTP-only cookie (packages/api/src/services/credentialCookie.js).
  • PENDING_REGISTRATION_KEY: Same loading rules as IRC_CREDENTIAL_KEY (separate secret). Used for AES-256-GCM of the deferred registration password (same wire format as the credential cookie: 12-byte IV + ciphertext + 16-byte auth tag, base64) and for HMAC-SHA256 of email verification codes (preimage anemoia:v1:email_verify:<userId>:<code>). Required at startup when REGISTRATION_MODE is email_code or approval.

OpenAPI

The API serves Swagger UI at /api/docs and the raw spec at /api/docs/json (YAML: /api/docs/yaml). The default server entry in the spec follows OPENAPI_SERVER_URL, then https://$DOMAIN, then http://127.0.0.1:$PORT. Use Authorize in Swagger UI with a Bearer token from login/register when testing protected routes.

Follow-ups (non-blocking improvements): polish-list.md.