HTTP API: server, credential, and the list of chats #4

Open
opened 2026-09-28 11:10:25 +02:00 by clawbot · 0 comments

Part of #2. First of four API units; the later ones add endpoints to what this one builds.

What. The bot serves an HTTP API beside the chat client. This unit builds the server, its credential check and its hardening, and the first endpoint: the list of chats.

Decisions (reversible, taken by the repo-manager):

  • Package internal/api; router github.com/go-chi/chi/v5, the decided default; the standard net/http server. No fx, Sentry or metrics: the bot has none and the issue does not ask for them.
  • Configuration, in internal/config, under its rule that a value set but unparseable aborts startup:
    • PORT: the API's TCP port, default 8080, on all interfaces inside the container. Anything but a whole number from 1 to 65535 aborts.
    • API_TOKEN_FILE: path of a file holding the API credential. Absent: the API still listens but refuses every request, and startup logs a warning saying so. Set: the file is read once at startup; unreadable, or shorter than 32 characters once surrounding whitespace is trimmed, aborts. The credential is never logged.
  • Authentication: Authorization: Bearer {credential}, compared with crypto/subtle.ConstantTimeCompare. Missing, malformed or wrong gives 401 with WWW-Authenticate: Bearer and {"error":"unauthorized"}. Trap: ConstantTimeCompare of two empty strings returns 1, so with no credential configured the check refuses before comparing; a test sends an empty bearer to a server with none configured. No path is exempt, not even a health check.
  • The chat client's WebSocket stays on 127.0.0.1:5225 inside the container; the image EXPOSEs only the API port.
  • Hardening per docs/REPO_POLICIES.md: ReadHeaderTimeout, ReadTimeout, WriteTimeout and IdleTimeout on the server; a per-request timeout; request bodies capped with http.MaxBytesReader at 64 KiB; on every response X-Content-Type-Options: nosniff, Content-Security-Policy: default-src 'none'; frame-ancestors 'none', X-Frame-Options: DENY, Referrer-Policy: no-referrer, Strict-Transport-Security: max-age=31536000; includeSubDomains, Cache-Control: no-store. No CORS headers. An error response is a chosen sentence as JSON, never an internal error's text, which goes to the log.
  • bot.Run starts the API after set-up and stops it, gracefully and within a bound, when it returns. The listener failing ends Run with an error, as the chat client failing does.
  • GET /api/v1/chats returns 200 and {"chats":[{"id":{contactId},"display_name":"..."}]}: the bot's direct chats, which are its contacts (it never joins groups), from the chat client's /_contacts {userId} command (contactsList). Further fields are the implementer's choice, documented.
  • API handlers call the chat client on the request's goroutine, never the event handler's (see EventHandler in internal/simplex/client.go).

Done when:

  • Tests cover the credential rules (none configured, wrong, right, empty bearer), parsing of both variables, the chats endpoint against the stand-in chat client or a fake, and the headers.
  • README: Configuration lists PORT and API_TOKEN_FILE. Getting Started creates the credential file on the volume, /var/lib/simplexcalc/api-token, mode 0600, from /dev/urandom with tools the image has, and runs with -p 127.0.0.1:8080:8080 -e API_TOKEN_FILE=/var/lib/simplexcalc/api-token. A new API section documents authentication and this endpoint with a curl example. Design mentions the API. docs/TODO.md Completed Steps gets a line.
  • make check is green.

Model: opus-5-5

Part of https://git.eeqj.de/sneak/simplexcalc/issues/2. First of four API units; the later ones add endpoints to what this one builds. **What.** The bot serves an HTTP API beside the chat client. This unit builds the server, its credential check and its hardening, and the first endpoint: the list of chats. **Decisions** (reversible, taken by the repo-manager): - Package `internal/api`; router `github.com/go-chi/chi/v5`, the decided default; the standard `net/http` server. No fx, Sentry or metrics: the bot has none and the issue does not ask for them. - Configuration, in `internal/config`, under its rule that a value set but unparseable aborts startup: - `PORT`: the API's TCP port, default `8080`, on all interfaces inside the container. Anything but a whole number from 1 to 65535 aborts. - `API_TOKEN_FILE`: path of a file holding the API credential. Absent: the API still listens but refuses every request, and startup logs a warning saying so. Set: the file is read once at startup; unreadable, or shorter than 32 characters once surrounding whitespace is trimmed, aborts. The credential is never logged. - Authentication: `Authorization: Bearer {credential}`, compared with `crypto/subtle.ConstantTimeCompare`. Missing, malformed or wrong gives `401` with `WWW-Authenticate: Bearer` and `{"error":"unauthorized"}`. Trap: `ConstantTimeCompare` of two empty strings returns 1, so with no credential configured the check refuses before comparing; a test sends an empty bearer to a server with none configured. No path is exempt, not even a health check. - The chat client's WebSocket stays on `127.0.0.1:5225` inside the container; the image `EXPOSE`s only the API port. - Hardening per `docs/REPO_POLICIES.md`: `ReadHeaderTimeout`, `ReadTimeout`, `WriteTimeout` and `IdleTimeout` on the server; a per-request timeout; request bodies capped with `http.MaxBytesReader` at 64 KiB; on every response `X-Content-Type-Options: nosniff`, `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`, `Strict-Transport-Security: max-age=31536000; includeSubDomains`, `Cache-Control: no-store`. No CORS headers. An error response is a chosen sentence as JSON, never an internal error's text, which goes to the log. - `bot.Run` starts the API after set-up and stops it, gracefully and within a bound, when it returns. The listener failing ends `Run` with an error, as the chat client failing does. - `GET /api/v1/chats` returns `200` and `{"chats":[{"id":{contactId},"display_name":"..."}]}`: the bot's direct chats, which are its contacts (it never joins groups), from the chat client's `/_contacts {userId}` command (`contactsList`). Further fields are the implementer's choice, documented. - API handlers call the chat client on the request's goroutine, never the event handler's (see `EventHandler` in `internal/simplex/client.go`). **Done when:** - Tests cover the credential rules (none configured, wrong, right, empty bearer), parsing of both variables, the chats endpoint against the stand-in chat client or a fake, and the headers. - README: Configuration lists `PORT` and `API_TOKEN_FILE`. Getting Started creates the credential file on the volume, `/var/lib/simplexcalc/api-token`, mode 0600, from `/dev/urandom` with tools the image has, and runs with `-p 127.0.0.1:8080:8080 -e API_TOKEN_FILE=/var/lib/simplexcalc/api-token`. A new API section documents authentication and this endpoint with a `curl` example. Design mentions the API. `docs/TODO.md` Completed Steps gets a line. - `make check` is green. Model: opus-5-5
clawbot added a new dependency 2026-09-28 11:10:29 +02:00
clawbot added a new dependency 2026-09-28 11:10:29 +02:00
clawbot self-assigned this 2026-09-28 11:10:30 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: sneak/simplexcalc#4