Web client (warp-web) with E2EE, and two sync fixes
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s

apps/web: a Matrix client for the browser on Vue 3, Tailwind and
matrix-js-sdk, built on @warp/ui. Like the admin panel it is an optional,
separate service: warp-web embeds the built app, serves /config.json and
proxies only /_matrix/client and /_matrix/media to the homeserver (or,
with -public-homeserver, lets the browser call it directly).
Sign-in and sign-up with registration tokens, room list, timeline with
back-pagination, text, images, video and files over authenticated media,
local echo with sent/read/failed states, receipts, typing, invites,
creating, joining and leaving rooms, room info, settings, phone layout.
End-to-end encryption through the Rust crypto: encrypted messages and
files, emoji verification, key backup setup and restore.

apps/ui: WRoomItem, WMessageBubble and WComposer from the brand package,
and the icons the client needs.

syncapi fixes found with the client:
- A room joined inside the sync window was sent as the join alone with
  limited: false, so clients thought they had the whole room. It is now
  read like an initial sync; a profile change is not mistaken for a join.
- A live typing notification was reported on every sync, so every long
  poll returned at once while anybody typed. Typing now has a position
  in the sync token and is reported when it changes, including the empty
  list when the last person stops.

Also: Dockerfile, a `web` compose profile, Makefile targets, a CI job
with a proxy smoke test, docs. PLAN.md moves out of the repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-18 16:22:11 +02:00
1 parent adc18e7cf3
commit b6f3be6ac4
90 files changed
+6608 -472

No files matched your search

+18
View File
@@ -117,6 +117,24 @@ warp-admin needs `/_warp/admin`, your public proxy can leave that path out
and keep the panel on an internal hostname. See
[apps/admin](../apps/admin/README.md).
## Web client
The web client is optional too, and separate from both: `warp-web` serves
it and, by default, proxies the client's Matrix requests
(`/_matrix/client` and `/_matrix/media`, nothing else) to the homeserver,
so the browser only ever talks to warp-web.
```sh
cd deploy/compose
docker compose --profile web up -d # http://localhost:8080
```
Or build the image (`make web-docker`) or the binary (`make web`) and point
it at the homeserver with `WARP_WEB_HOMESERVER`. If your homeserver is
already public behind its own proxy, set `WARP_WEB_PUBLIC_HOMESERVER` to
its URL instead: the browser then calls it directly and warp-web serves
only the client. See [apps/web](../apps/web/README.md).
## Reverse proxy
Behind a proxy that terminates TLS:
+33 -7
View File
@@ -42,7 +42,7 @@ second place for them to disagree.
## Stream tokens
A sync response is assembled from seven streams that move independently:
A sync response is assembled from eight streams that move independently:
| Stream | Moves when | Position from |
|---|---|---|
@@ -53,18 +53,19 @@ A sync response is assembled from seven streams that move independently:
| presence | somebody's presence actually changes | `sync_sequences` |
| device list | somebody's end-to-end keys change ([e2ee.md](e2ee.md)) | `keyserver_sequences` |
| notification | a user's unread counts in a room move ([push.md](push.md)) | `push_sequences` |
| typing | somebody starts or stops typing, or a notification runs out | a counter in memory (see [Typing](#typing)) |
A client that has seen receipt 7 but only event 4 has to be able to say so
in the one opaque string the API gives it, so a token names a position in
each:
```
s{pdu}_{receipt}_{to_device}_{account_data}_{presence}_{device_list}_{notification}
s{pdu}_{receipt}_{to_device}_{account_data}_{presence}_{device_list}_{notification}_{typing}
```
Trailing zeroes are left off, so a client that has only ever seen room
events carries `s12` rather than `s12_0_0_0_0_0_0`. The device list and
notification positions came later (Phases 7 and 8) and were added at the
events carries `s12` rather than `s12_0_0_0_0_0_0_0`. The device list,
notification and typing positions came later and were added at the
end. A token with fewer
positions than this build writes is accepted and the missing ones read as
zero — which is what lets a token stored before a stream existed go on
@@ -167,6 +168,21 @@ cases, which `roomserver.syncState` implements:
A timeline that reaches back to the room's create event reports no state at
all: there is nothing before it.
### A room joined inside the window
To the client, a room the user has just joined is new: it has none of its
history and none of its state. So an incremental sync reads such a room
the way an initial sync does — the recent history the user may see and the
whole state before it — instead of the events since `since`, which would
be the join alone with `limited: false`, telling the client it has
everything.
"Joined inside the window" means the user's latest membership event is in
the window *and* the membership before it was not already `join`. A
display name or avatar change is a new join event too, but resending the
room's history for one would be wrong; `roomserver.MembershipBefore` tells
the two apart.
## Visibility
`roomserver.VisibleRange` answers "what span of this room may this user
@@ -331,9 +347,19 @@ having stopped typing, and a second process would not see them. The second
only matters when Warp stops being one process, which PLAN.md says is not
the plan.
They therefore have no stream position. A notification that started and
stopped between two syncs is simply never reported; one that is live is
reported on every sync until it expires.
Their stream position is a counter in the tracker that moves on every
change, with each room remembering the position of its last one. A sync
reports a room's typing list when it changed inside the window — including
to empty, which is how a client hears that somebody stopped — and not
again while it stays the same. Reporting a live notification on every sync
instead would make every long poll return at once while anybody typed: a
busy loop that ends in the rate limiter.
The counter starts at zero again after a restart, so a client's typing
position is compared for difference, never for order: one the tracker has
never reached is from before the restart, and every room it knows of
counts as changed. A notification that started and stopped between two
syncs is reported as the empty list, which is what the client already had.
### Receipts