v2.4.0

Security and availability fixes for the AI proxy and server-side rendering,
plus the beneficiary lines in the editor (MRs !168, !170, !171, !172, !173,
!174, !175).

- Security: the AI proxy took the upstream endpoint from the request body and
  passed it to fetch unchecked, so anything the server could reach a caller
  could reach through it, with the response returned verbatim. Endpoints now go
  through a guard that allows https only, refuses addresses that are not
  publicly routable, and rejects redirects, checking the address inside the
  connection's DNS lookup so it covers the one actually connected to. The
  routes also require the same CSRF token the rest of the API does, which stops
  the proxy being usable as an open relay towards any public host. node-fetch
  becomes a direct dependency at 2.7.0: it was present only transitively at
  1.7.3, which leaks the Authorization header on a cross-host redirect.
- The AI proxy had never worked in a container. fetch was used but never
  imported, and the image runs Node 16, which has no global fetch, so every
  call answered 500. Fixing that alone would have armed the hole above, so both
  land together.
- Rate limiting: rateLimitReq ignored its own limit argument, keyed on a header
  the client writes, and never expired its entries. It now takes a per-minute
  budget counted per route over a sliding window, identifies the visitor by
  CF-Connecting-IP where the deployment declares it is behind Cloudflare, and
  evicts the oldest entries at its ceiling rather than turning new callers away.
- Availability: a failing remote document no longer kills a worker. The pinned
  posts, dapps and nsfw lists are fetched from raw URLs, and a redirect, an
  HTML error page or a rate-limited RPC each produced an uncaught exception or
  a promise that never settled. Fetching is now one helper that always settles,
  entries that cannot be read are skipped rather than failing the batch, each
  loader keeps its last good value, and an unhandled rejection is logged
  instead of ending the worker.
- The AI model picker asks the provider which models the key may use instead of
  offering a list compiled into the client, which had gone stale to the point
  where every option failed. The model can always be typed. Upstream failures
  answer 400 with the provider's message rather than a status a CDN replaces
  with its own error page.
- The AI chat now streams: the event stream was being gzipped, which held the
  answer back until it was complete. Reasoning models no longer print their
  chain of thought as the answer, the settings screen and the chat share one
  configuration store, and code spans are legible in the dark theme.
- Logging out clears the assistant. Its conversation and the provider API key
  live in the browser, so both survived a logout and the next person at that
  machine could read the chat and spend the key. Expanding the chat window no
  longer hides its own toolbar behind the site header.
- Editor: the beneficiary lines print a total when more than one automatic
  share applies and name the origin of each, instead of showing two identical
  lines that contradicted the counter. Translated into the remaining locales.