Commit Graph
4 Commits
Author SHA1 Message Date
tajniak81andClaude Opus 5.5 92836f8dcd Phase 6: media
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
Implements PLAN.md Phase 6: the media repository. Clients upload files,
reserve an mxc:// URI and upload to it later, download and ask for
thumbnails, all through the authenticated media endpoints of Matrix 1.11;
other servers download this server's media through the federation media
endpoints, and media uploaded elsewhere is fetched over federation on
first use and kept.

internal/mediaapi is the component. Content goes into a BlobStore named
by its SHA-256, so the same file uploaded twice is stored once and the
hash doubles as the ETag; the metadata that maps an mxc:// URI onto a
blob is in media_repository and media_thumbnails. The local-filesystem
store writes to a temporary file and renames it into place once hashed,
so nothing ever refers to half a file. A storage driver can bring its own
store through storage.BlobProvider, which is where PocketBase's
filesystem abstraction (local or S3) plugs in with Phase 10.

Reserved uploads belong to the user who made them, take one upload,
lapse after a day, and are capped per user; a download of one waits for
its content, woken in memory the moment it arrives, for as long as
timeout_ms allows.

Thumbnails come from a fixed set of configured sizes, so a thousand
requested sizes cannot mean a thousand resizes. They are made on first
request and kept, never enlarge, honour a JPEG's EXIF orientation, and
point at the original when it already fits. The pixel count is checked
from the header before anything is decoded, at most four resizes run at
once, and concurrent requests for the same thumbnail share one.

Remote media is fetched through the signed client: the multipart/mixed
answer of Matrix 1.11, with a fallback to the legacy download endpoint
for servers that answer M_UNRECOGNIZED, and Location redirects followed
by a dialer that refuses loopback, private and link-local addresses.
Concurrent requests share one fetch, detached from the request that
started it. With federation off nothing is fetched, and what was cached
before is still served.

Downloads carry a sandboxing CSP, nosniff, inline Content-Disposition
only for the types the spec lists as safe, and private immutable caching.
The unauthenticated legacy download endpoints are off by default behind
media.legacy_unauthenticated_downloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:26:49 +02:00
tajniak81andClaude Opus 5 c96f88e994 Phase 3: rooms, local only
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
Implements PLAN.md Phase 3: the roomserver, the room half of storage, the
Client-Server room endpoints and the admin room endpoints. A client can now
create a room, invite, join, leave, kick, ban, send messages and state,
redact, name a room with an alias and find one in the public directory.
Nothing talks to another server; every operation that would need to is
refused with a message that says so rather than a bare M_FORBIDDEN.

roomserver: one input pipeline every event goes through, whatever sent it —
validate, authorise, resolve, persist, emit. The rules are not
reimplemented here; internal/matrix is pure library code and this is what
connects it to a database and to a room's history. The Client-Server
handlers do no authorisation of their own, deliberately: whether Alice may
kick Bob is a question about the room's power levels, and the one place
that is answered correctly is the authorisation rules inside the pipeline.

A mutex per room serialises writers. Two events racing into one room would
each read the same forward extremities, both become one, and fork the room
for no reason — which would then need state resolution to put back
together. State resolution is wired in anyway for the multi-extremity
case. It is unreachable from outside in this phase, and the alternative is
a pipeline that authorises against one arbitrary branch of a fork without
saying so.

Room versions 3 to 12 all work and are each tested end to end. Version 12
earns that coverage: its room ID is the create event's own ID, its create
event carries no room_id, and the create event is excluded from every other
event's auth_events — so the state auth.Check is given is the event's
auth_events plus the create event, which that version identifies by room ID
rather than citing.

createRoom sends the spec's events in the spec's order, each authorised
against what the ones before it produced. A failure part-way leaves an
incomplete room, which is the lesser evil: the alternative is holding every
event of a new room in memory and writing them in one transaction, making
the pipeline that authorises them a special case. The one failure a second
client can cause at any time, an alias clash, takes the room row back out.

Redactions stay two decisions, as the spec keeps them. The rules let almost
any redaction into the room; whether it applies to its target is a separate
test, run inside the same transaction as the redaction itself so that a
client holding an event ID can always read the stripped event.

storage: events keyed by event ID rather than by a surrogate, since an
event ID appears in prev_events, auth_events, state snapshots and client
requests alike. Stream ordering comes from a counter row bumped inside the
write transaction, because a PostgreSQL sequence is explicitly not
transactional and a rolled-back write would leave a hole in the stream that
/sync would have to be taught to skip. State snapshots are stored per event
and deduplicated by content hash, so the table grows with state changes
rather than with traffic — and that is what makes "the state after a
forward extremity" answerable at all. The room's listing columns are
denormalised onto its row because the public directory orders every
published room by member count and pages the result, which cannot be done
by reading each room's state in turn.

Federation-disabled behaviour from PLAN.md section 3 is enforced here.
Restricted joins are the exception that works: the rule needs a signature
from the authorising user's server verified, and while federation is off
that server is always this one.

adminapi: room list and detail, purge, force-leave, and room and event
counters on the dashboard. clientapi gains its own rate-limit buckets for
createRoom and for joining, both of which write events rather than read
them.

Tested against the real event core and a real database rather than fakes:
what is under test is whether an event this server signs is one the
authorisation rules accept, and a fake of either would only prove that the
pipeline calls what the fake expects. The storage tests run against both
SQL drivers. docs/rooms.md describes the pipeline, the storage decisions
and the known gaps — chief among them that history visibility is
room-level rather than per-event, and that /sync still reports no rooms,
both of which belong with the event stream in Phase 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 10:34:57 +02:00
tajniak81andClaude Opus 5 6004bb01de Phase 1: accounts and sessions
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
Implements PLAN.md Phase 1. A client can now register, log in, manage its
devices and set its profile; an operator can create accounts and flip the
runtime switches. There are still no rooms and no real /sync.

userapi: accounts, argon2id passwords in PHC format, devices, access and
refresh tokens stored as SHA-256 hashes, account data and the admin flag.
Its storage interface is storage.UserStore, implemented once in sqlstore
with `?` placeholders; the postgres driver supplies a rebind function and
unique-violation recognition rather than keeping a second copy of thirty
statements. Timestamps in these tables are int64 milliseconds, the unit the
Client-Server API uses on the wire.

Client-Server API: register (user-interactive auth with m.login.dummy and
optional registration tokens), register/available, token validity, login
with m.login.password, logout, logout/all, refresh, account/whoami,
capabilities, devices/* with password-gated deletion, profile/*, global and
per-room account data.

GET /sync is a deliberate placeholder: it answers a valid token with an
empty but well-formed response and refuses a revoked one. Phase 1 has no
rooms, so that is the whole truth, and it is what makes session revocation
observable. Phase 4 replaces it with the syncapi.

Auth middleware resolves a bearer token (header or the legacy query
parameter) to a device and user, answering an expired token with the soft
logout the spec asks for. Rate limiting is a token bucket per endpoint class
keyed on the client address, returning M_LIMIT_EXCEEDED with retry_after_ms.

adminapi v1 under /_warp/admin/v1: admin-only auth via a normal Matrix
token, GET/PUT /federation and /registration driving internal/settings and
reporting a config-pinned switch as a 409, GET/POST /users, GET /stats.

warp create-account [--admin] creates the first account straight in the
database, so a new install with registration off has a way in.

Complement is wired up for the federation=off profile: an image meeting its
contract, an entrypoint that configures Warp entirely from WARP_*
variables, and a runner that builds, clones and executes the target set
(TestRegistration, TestLogin, TestDeviceManagement). It has not been run
yet -- there is no container runtime on the development machine -- so the
three tests' assertions were read from the Complement sources and pinned as
httptest tests instead, and CI gained a job that runs the real thing.

Also fixes a .gitignore bug from Phase 0: the unanchored `warp` pattern,
meant for the built binary, also matched the directory server/cmd/warp and
silently excluded the entire command package from the repository. The
binary patterns are now anchored to the repository root, and cmd/warp is
committed for the first time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 18:18:37 +02:00
tajniak81andClaude Opus 5 b4fb60a8ab Phase 0: bootstrap the Warp homeserver
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 / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Scaffolds the server module and everything Phase 0 of PLAN.md calls for:
a binary that boots, serves its discovery endpoints and nothing else yet.

- config: one YAML file plus per-field WARP_* environment overrides,
  unknown fields rejected, validation that reports every problem at once
- logging and metrics: slog, a Prometheus registry, per-route HTTP
  instrumentation labelled by pattern rather than path
- httputil: the Matrix error envelope, error-returning handlers, CORS,
  request logging, panic recovery, and a router over http.ServeMux
- endpoints: /_matrix/client/versions, /.well-known/matrix/client,
  /.well-known/matrix/server (gated), /healthz, /metrics, and
  M_UNRECOGNIZED for everything else
- storage: driver registry, shared database/sql base, checksummed
  migration runner, sqlite and postgres drivers, pocketbase stub
- settings: server_settings table behind a cached service, so the
  federation and registration switches change without a restart
- federation: the gate that makes federation a runtime mode
- cmd/warp: serve, migrate, generate-keys, version
- Makefile, golangci-lint v2, GitHub Actions CI (Linux and Windows tests,
  race on Linux, storage tests against PostgreSQL, lint, boot smoke test),
  Dockerfile, example config and docs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 17:01:13 +02:00