Files
Warp/docs/development.md
T
tajniak81andClaude Opus 5.5 2fbd5f53ce
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
Phase 2: the event core
Implements PLAN.md Phase 2: the library every later phase is built on,
as pure code under server/internal/matrix with no HTTP and no storage.

canonicaljson: the canonical JSON encoding, hand-written because
encoding/json escapes <, > and & and accepts floats. Integers only, in
[-(2^53)+1, 2^53-1]; lone surrogate escapes rejected rather than turned
into U+FFFD.

signing: the Signing JSON algorithm alongside the existing key handling,
and one lenient base64 decoder shared by the packages above it.

id: identifier grammars as they appear inside events, deliberately laxer
than userapi's account rules because rooms carry historical user IDs.

roomversion: the registry. Versions 3 to 12 are supported, each written
out in full as a Rules value with the flags the other packages read.
Versions 1 and 2 are known but refused: their event format and state
resolution v1 wait for federation. createRoom will default to 11, as the
spec asks for a while after 1.16, with 12 available.

event: PDU parsing and validation, content and reference hashes, event
IDs, the redaction algorithm for every supported version, signing and
verification, and a Builder.

auth: the authorisation rules in spec order, power levels with their
defaults, v12 creators with infinite power, the auth events selection
algorithm, rule 3's auth_events checks, and the separate test for
whether a redaction applies. Comments use the room version 12 rule
numbering, with the version 11 numbers given where they differ.

stateres: state resolution v2, and version 12's amendment (an empty
starting state and the conflicted state subgraph).

Tested against the spec's own vectors byte for byte: the canonical JSON
examples, both JSON signing vectors and both event signing vectors.
State resolution ports scenarios from Synapse's suite. canonicaljson
and event.Parse have fuzz targets.

The capabilities endpoint now advertises the registry instead of a
hard-coded "11". docs/event-core.md describes the packages and the
known gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:23:35 +02:00

167 lines
6.4 KiB
Markdown

# Development
## Prerequisites
- Go 1.26+. Nothing else for the default build: the SQLite driver is pure
Go and `CGO_ENABLED=0` works everywhere.
- A C toolchain only if you want `go test -race`.
- PostgreSQL only to run the storage tests against it.
- `golangci-lint` v2 for `make lint`:
`go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest`
## Everyday commands
```sh
make build # bin/warp
make test # go test ./... in server/
make check # vet + test
make lint
make cover # coverage.html
make clean
```
Each one is a two-line shell command if you would rather not use `make`;
look at the [Makefile](../Makefile).
## Running a server locally
```sh
cp warp.yaml.example warp.yaml # set global.server_name
make build
./bin/warp generate-keys
./bin/warp serve
```
The SQLite file, the signing key and (later) the media store are written
relative to the working directory. They are in `.gitignore`.
For a throwaway server with no config file at all:
```sh
WARP_SERVER_NAME=dev.local WARP_PRIVATE_KEY=./keys/dev.key ./bin/warp generate-keys
WARP_SERVER_NAME=dev.local WARP_PRIVATE_KEY=./keys/dev.key ./bin/warp serve
```
## Adding a database migration
1. Write the same change twice, once per SQL dialect:
`server/internal/storage/sqlite/migrations/NNNN_name.sql` and
`server/internal/storage/postgres/migrations/NNNN_name.sql`.
Use the next free number; `NNNN` is zero-padded to four digits.
2. The files are embedded with `embed.FS`, so a rebuild picks them up.
3. `./bin/warp migrate --status` lists what would run, `./bin/warp migrate`
applies it. A server started with `database.auto_migrate: true` (the
default) runs them itself.
Rules the runner enforces:
- One transaction per migration. A failure rolls that file back entirely
and leaves the following ones unapplied.
- A migration that has already been applied and whose file has since
changed is an error. Write a new migration instead of editing an old
one; the checksum of every applied file is recorded.
- A database that has migrations this binary does not know about refuses
to start, so an old binary cannot run against a newer schema.
## Adding an endpoint
Routes are registered in `server/internal/setup/routes.go` on an
`httputil.Router`, which is `http.ServeMux` plus per-route metrics:
```go
r.HandleFunc("GET /_matrix/client/v3/profile/{userID}/displayname", s.handleGetDisplayName)
```
Handlers have the signature `func(http.ResponseWriter, *http.Request) error`
and return Matrix errors directly:
```go
func (s *Server) handleThing(w http.ResponseWriter, r *http.Request) error {
if somethingWrong {
return httputil.Forbidden("You are not allowed to do that")
}
httputil.WriteJSON(w, http.StatusOK, response{})
return nil
}
```
Anything returned that is not a `*httputil.MatrixError` is logged in full
and reported to the client as `M_UNKNOWN`, so internal detail never leaks.
Endpoints that belong to the federation surface are registered through the
gate instead, so they disappear when federation is switched off:
```go
r.Handle("GET /_matrix/federation/v1/version", s.federation.ProtectFunc(s.handleFederationVersion))
```
A Client-Server endpoint that would need federation to answer returns
`federation.RejectClientRequest("...")`, which is an `M_FORBIDDEN` that
says why.
## Tests
- Unit tests live next to the code. Each API package holds its own
HTTP-level tests, built on a throwaway SQLite database with the real
components behind them rather than fakes; `internal/setup` holds the
smaller set that goes through the whole wrapped handler, to catch a route
that was written but never registered.
- Storage tests run against every driver that is available:
SQLite always, PostgreSQL when `WARP_TEST_POSTGRES_DSN` is set.
- The event core under `internal/matrix` is tested against the spec's own
cryptographic and canonical JSON test vectors, and has fuzz targets for
the two functions that read bytes straight off the network. `make test`
runs the unit tests; the fuzzers are opt-in. See
[event-core.md](event-core.md).
- CI runs `gofmt -l`, `go mod tidy` (as a diff check), `go vet`, the tests
on Linux and Windows, the race detector on Linux, `golangci-lint`, and a
smoke test that boots the real binary and curls it.
## Complement
[Complement](https://github.com/matrix-org/complement) is the official Matrix
integration suite. It runs a container per homeserver per test and drives it
over the real HTTP API, which makes its pass count the honest measure of how
far along the server is — PLAN.md section 9 treats it as the primary progress
metric.
```sh
make complement # the current target set
server/test/complement/run.sh TestLogin # one test
COMPLEMENT_DEBUG=1 make complement # with the container logs
```
It needs Docker. The script builds the image, clones Complement into
`.complement/` (ignored by git) and runs the subset for the phases that are
done — currently `TestRegistration`, `TestLogin` and `TestDeviceManagement`,
with federation off. Extend `COMPLEMENT_TESTS` in
[run.sh](../server/test/complement/run.sh) as each phase lands, rather than
switching to the whole suite: a pass count that drops because a later phase
is missing is noise.
The image is [server/test/complement/Dockerfile](../server/test/complement/Dockerfile)
and the deployment is entirely `WARP_*` environment variables — see
[entrypoint.sh](../server/test/complement/entrypoint.sh). Two things there
are worth knowing: rate limiting is off, because the whole suite comes from
one address and would otherwise spend its time reading `M_LIMIT_EXCEEDED`;
and port 8448 is exposed but nothing listens on it yet. The federation
profile needs a TLS listener signed by the CA that Complement mounts at
`/complement/ca`, which arrives with federation in Phase 5.
## Creating the first account
A new install has registration off, so the first account is made from the
command line against the database directly — no server needs to be running:
```sh
./bin/warp create-account --username you --admin
```
It prompts for the password twice. `--password-file` reads it from a file and
`--password` takes it inline, for scripts that have already thought about the
shell history. From there, the admin API can turn registration on:
```sh
curl -X PUT http://localhost:8008/_warp/admin/v1/registration -H "Authorization: Bearer $TOKEN" -d '{"enabled": true}'
```