Phase 9: application services and admin completion
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 9. internal/appservice reads registration files
(appservice.config_files), matches namespaces the way Synapse does
(anchored at the start), and delivers events to each service as
transactions read from the roomserver's event stream at a cursor kept in
appservice_state. A transaction in flight is recorded, so after a restart
it is resent under the same ID; failures back off from 2s to 5m, and a
successful ping cuts the backoff short.

In the Client-Server API an as_token acts as the service's sender or, with
?user_id=, as any existing user in its namespace. Services register and
log in their users with m.login.application_service; exclusive user and
alias namespaces are refused to everybody else with M_EXCLUSIVE. Unknown
aliases and users in a namespace are asked of the service first. The
ping endpoint reports M_URL_NOT_SET, M_BAD_STATUS, M_CONNECTION_FAILED
and M_CONNECTION_TIMEOUT. Services' own requests, and their users' when
rate_limited is false, skip the rate limiter.

Users can change their password (POST /account/password) and deactivate
their account (POST /account/deactivate), both behind the password.
Deactivation revokes tokens, deletes devices, optionally erases the
profile and leaves every room through a new userapi.AccountObserver.

The admin API is complete: user detail, the admin flag, deactivation and
reactivation, password resets, devices, media quotas
(media.max_user_storage_bytes plus per-user overrides), registration
tokens with use limits and expiry (registration.require_token), server
notices (internal/servernotices, server_notices.*), an editor for the
federation allow/deny lists persisted in server_settings, a delivery view
of the application services, and an audit log of every change.

Migration 0011 adds the registration token, server notice room, media
quota, audit log and appservice cursor tables. Policy lists are now
returned sorted. The Complement entrypoint loads the registrations
Complement mounts in /complement/appservice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-17 12:58:28 +02:00
1 parent b507753172
commit a94794b866
69 files changed
+6922 -124

No files matched your search

+1
View File
@@ -11,6 +11,7 @@
| [media.md](media.md) | `internal/mediaapi`: uploads, downloads, thumbnails, other servers' media, where the files go |
| [e2ee.md](e2ee.md) | `internal/keyserver`: device and one-time keys, cross-signing, device list tracking, key backup |
| [push.md](push.md) | `internal/pushrules` and `internal/pushserver`: push rules, notification counts, pushers, the push gateway client |
| [appservices.md](appservices.md) | `internal/appservice`: registration files, namespaces, acting as users, delivering events to bridges |
| [admin-api.md](admin-api.md) | `/_warp/admin/v1`: the operator API behind the admin panel |
The design and roadmap are in [PLAN.md](../PLAN.md) at the repository root.
+285 -13
View File
@@ -44,7 +44,8 @@ The runtime federation switch of PLAN.md section 3.
"enabled": false,
"toggleable": true,
"allowlist": [],
"denylist": []
"denylist": [],
"lists_edited": false
}
```
@@ -54,8 +55,28 @@ and answers with the same document. `toggleable` is `false` when
`PUT` then returns `409` and the panel should grey the switch out rather
than offer something that will bounce.
Editing the allow and deny lists is Phase 9. They are reported now because
the dashboard has to show which servers a running Warp will talk to.
The lists are lower-cased and sorted. `lists_edited` is `true` when they
were set through `PUT /federation/lists` and replace the config file's.
### `PUT /federation/lists`, `DELETE /federation/lists`
```json
{"allowlist": ["friend.example"], "denylist": []}
```
Replaces both lists. They take effect on the next request in either
direction, and are kept in `server_settings` in place of the config
file's, across restarts. Both have to be in the body (`[]` for an empty
one): a panel that sent one and had the other silently kept would be one
bug away from leaving a server it meant to cut off on the allowlist. A
name that is not a server name is `400 M_INVALID_PARAM`.
`DELETE` forgets the edited lists and puts the config file's back. Both
answer with the `GET /federation` document.
Unlike the switch, the lists are editable whatever
`federation.allow_runtime_toggle` says: they are not a switch the file can
pin.
### `GET /federation/destinations`
@@ -138,6 +159,259 @@ the admin's own action, not a public sign-up. `username` is normalised
(lower-cased) like any other localpart. A taken name is
`400 M_USER_IN_USE`, an unusable one `400 M_INVALID_USERNAME`.
### One user: `/users/{userID}`
`{userID}` is a full user ID of this server, `@alice:warp.example`. A user
of another server is `400 M_INVALID_PARAM`, one that does not exist
`404 M_NOT_FOUND`.
`GET /users/{userID}` is the list row plus what it takes a count to know:
```json
{
"user_id": "@alice:warp.example",
"display_name": "Alice",
"admin": false,
"deactivated": false,
"created_at_ms": 1760000000000,
"devices": 2,
"joined_rooms": 5,
"media": {"uploads": 12, "bytes": 3145728, "quota_bytes": 104857600, "custom_quota": false}
}
```
`joined_rooms` is absent on a server without a signing key, which has no
rooms. Every change below answers with this document, so the panel can
redraw from the response.
| Endpoint | Body | What |
|---|---|---|
| `PUT /users/{userID}/admin` | `{"admin": true}` | grant or revoke the admin flag |
| `POST /users/{userID}/deactivate` | `{"erase": false}` (optional) | deactivate the account |
| `POST /users/{userID}/reactivate` | — | let it log in again |
| `POST /users/{userID}/password` | `{"password": "...", "logout_devices": true}` | set a new password |
**Deactivation** does what the user's own `POST /account/deactivate` does.
Every token stops working at once; the devices go, with their keys and
pushers; the user leaves every room, rejects every invite and withdraws
every knock. With `erase`, the display name and avatar are cleared first,
so the leave events carry neither. The name stays taken: a new user with
the old user's ID would inherit what other servers and clients remember
about the old one. Deactivating a deactivated account changes nothing.
**Reactivation** gives back the ability to log in, with the old password
unless a new one is set. Nothing else comes back: the rooms were left and
the devices are gone.
**A password reset** logs every device out unless `logout_devices` is
`false`. An admin resetting a password is usually doing it because
somebody else has the account.
An admin cannot revoke their own admin flag or deactivate themselves
(`403`). Both are one click from locking the last admin out of the panel,
and both are easy to do by accident on a table of users. Another admin can
still do either.
### `GET /users/{userID}/devices`, `DELETE /users/{userID}/devices[/{deviceID}]`
```json
{"devices": [{"device_id": "QWERTYUIOP", "display_name": "Phone", "created_at_ms": 1760000000000,
"last_seen_ms": 1760600000000, "last_seen_ip": "203.0.113.4", "user_agent": "Element/1.11"}]}
```
`DELETE` on one device logs that session out, taking its keys and pushers
with it. An unknown device is `404`. On the collection it logs out every
device. Unlike the client's own device deletion, no password is asked for:
the admin token is the authority here.
### `GET /users/{userID}/media`, `PUT`/`DELETE /users/{userID}/media/quota`
```json
{"uploads": 12, "bytes": 3145728, "quota_bytes": 104857600, "custom_quota": false}
```
What the user has uploaded, against their quota. Every upload counts at
its own size, even when two are the same bytes and the server keeps one
copy: a quota is about what a user asked this server to keep.
`quota_bytes` is the quota in force, `0` for none. `custom_quota` says
whether it was set for this user or comes from
`media.max_user_storage_bytes`.
`PUT {"max_bytes": 1073741824}` gives the user a quota of their own (`0`
for no limit). `DELETE` puts them back on the configured one. A quota below
what the user already has deletes nothing; it stops further uploads until
they are back under it.
An upload that would go over is refused as it arrives, with
`413 M_TOO_LARGE`. Every client already tells the user an upload was too
big, and that is what it was. Two uploads racing for the last of a quota
can both get in: a quota bounds what a user keeps, it is not an
accounting system.
### Registration tokens
Tokens with a use limit and an expiry, for the `m.login.registration_token`
stage of sign-up. The stage is in the flow when `registration.tokens` is
non-empty or `registration.require_token` is on. The config file's tokens
and these are both accepted. The config file's are never listed here:
they are in the config file, and this would be the one place a secret
from it is served over HTTP.
```
GET /registration_tokens
POST /registration_tokens {"token": "...", "length": 16, "uses_allowed": 5, "expiry_time_ms": 1760000000000}
GET /registration_tokens/{token}
PUT /registration_tokens/{token} {"uses_allowed": null, "expiry_time_ms": 1760000000000}
DELETE /registration_tokens/{token}
```
A token:
```json
{
"token": "invite-1",
"uses_allowed": 5,
"completed": 2,
"expiry_time_ms": null,
"created_at_ms": 1760000000000,
"valid": true
}
```
`uses_allowed` and `expiry_time_ms` are `null` for no limit. `valid` is
whether a registration could use the token now. The list also carries
`"required"`, whether sign-up asks for a token at all. A token made on a
server that does not ask is never needed.
`POST` with no `token` generates one of `length` characters (16 by
default) from `A-Za-z0-9`. A chosen token is 1 to 64 of the characters
`A-Za-z0-9._~-`, the spec's grammar. A taken or malformed one is
`400 M_INVALID_PARAM`. It answers `201`.
`PUT` changes only the fields in the body; `null` removes a limit. Raising
`uses_allowed` on a used-up token makes it usable again. `DELETE` does not
affect accounts registered with the token.
A use is spent when the account is created, not when the stage is passed,
so an abandoned sign-up costs nothing. Checking and spending are one
statement, so two sign-ups racing for the last use cannot both have it.
The one that loses is refused with `403 M_FORBIDDEN` at the end of its
flow.
### `POST /server_notices`
```json
{"user_id": "@alice:warp.example", "content": {"msgtype": "m.text", "body": "Maintenance at 22:00 UTC."}}
```
Sends a message from the server to one of its users, and answers
`{"room_id": "...", "event_id": "..."}`. Needs `server_notices.enabled`
and a signing key. Without them it is `409`, the server's configuration
refusing.
Each user gets one notice room. It is made the first time, by the notices
user (`server_notices.localpart`, created at startup), and carries the
`m.server_notice` tag in the user's room account data, so a client can
show it apart from other rooms. A second notice goes to the same room. A
user who left is invited back. The user cannot answer: the power levels
give them none of the levels a message needs. The user does not have to be
online: the invite and the message wait for them.
`content.body` is required. A missing `msgtype` becomes `m.text`. An
unknown user is `404`; a deactivated one `403`.
### `GET /appservices`
The registered application services and how delivery to each is going.
Registrations are files read at startup, so this is a view, not an editor.
Tokens are never shown.
```json
{
"appservices": [
{
"id": "telegram",
"url": "http://localhost:29317",
"sender": "@telegrambot:warp.example",
"rate_limited": false,
"protocols": [],
"namespaces": {
"users": [{"regex": "@telegram_.*:warp\\.example", "exclusive": true}],
"aliases": [],
"rooms": []
},
"delivery": {
"stream_pos": 1204,
"behind": 0,
"txn_id": 88,
"events_delivered": 412,
"failures": 0,
"last_success_ms": 1760600000000,
"last_failure_ms": 0,
"retry_at_ms": 0
}
}
]
}
```
`delivery` is absent for a service without a URL, which is sent nothing.
`behind` is how many stream positions it has still to read: not all of
them will concern it. While `failures` is above zero the service is being
backed off, `last_error` says why, and `retry_at_ms` is the next attempt.
See [appservices.md](appservices.md).
### `GET /audit_log`
```
GET /audit_log?limit=50&before=1234&actor=@root:warp.example&action=user.deactivate
```
Every change made through the admin API, newest first:
```json
{
"entries": [
{
"id": 1235,
"ts_ms": 1760600000000,
"actor": "@root:warp.example",
"action": "user.deactivate",
"target": "@mallory:warp.example",
"details": {"erase": true}
}
],
"next_before": 1235
}
```
The log is paged by ID, not by offset. It grows at the top while an admin
reads it, and an offset would shift under them. Pass `next_before` as
`?before=` for the next page; it is absent on the last one. `actor` and
`action` filter exactly.
The actions:
| Action | Target | Details |
|---|---|---|
| `federation.switch` · `registration.switch` | — | `enabled` |
| `federation.lists` · `federation.lists_reset` | — | `allowlist`, `denylist` |
| `federation.destination_reset` | server name | — |
| `user.create` | user | `admin` |
| `user.admin` | user | `admin` |
| `user.deactivate` · `user.reactivate` | user | `erase` |
| `user.password` | user | `logout_devices` |
| `user.device_delete` | user | `device_id`, or `all` |
| `user.media_quota` | user | `max_bytes` (`null` for a reset) |
| `room.purge` · `room.force_leave` | room | `user_id` |
| `registration_token.create` · `.update` · `.delete` | token | `uses_allowed`, `expiry_time_ms` |
| `server_notice.send` | user | `room_id`, `event_id` |
An entry is written after the change has been made. If the write fails,
that is logged and the request still succeeds: the change has happened,
and a `500` for something that worked would have the panel retry it. The
same information is in the server log either way.
### `GET /rooms`
```
@@ -259,14 +533,12 @@ configuration is what refuses it.
## Not here yet
Phase 9 completes the admin API: deactivating users, resetting passwords,
revoking devices, granting the admin flag, registration token management,
the federation allow/deny list editor, server notices, media quotas and an
audit log of admin actions.
The room endpoints are still the Phase 3 set. Still missing: a room's
state and recent events, for an operator looking at a report, and a way to
publish or withdraw a room from the directory without borrowing a member's
token. Media has quotas but no purge or quarantine yet; see
[media.md](media.md#known-gaps).
The room endpoints will grow with it: a room's state and its recent events
for an operator looking at a report, and a way to publish or withdraw a room
from the directory without borrowing a member's token.
A server started without a signing key has no roomserver, and the four room
endpoints then answer `503` rather than failing on the first call.
A server started without a signing key has no roomserver. The four room
endpoints then answer `503` rather than failing on the first call, and
server notices answer `409`.
+253
View File
@@ -0,0 +1,253 @@
# Application services
`server/internal/appservice`, and the Client-Server half of it in
`server/internal/clientapi/appservice.go`: PLAN.md Phase 9. An application
service is a bridge or a bot with a namespace of its own: users, aliases
and rooms it may act for, and the events about them delivered to it as
they happen. This is the
[Application Service API](https://spec.matrix.org/latest/application-service-api/).
| Package | What |
|---|---|
| `appservice` | Registration files, namespace matching, the registry, the HTTP client that talks to services, and the sender that delivers events to them. |
| `clientapi` | A service's token as an access token, acting as its users with `?user_id=`, registering and logging in its users, the `M_EXCLUSIVE` refusals, the ping endpoint, and asking services about users and aliases that do not exist. |
## Registering a service
Every bridge writes a registration file. Point Warp at it:
```yaml
appservice:
config_files:
- /etc/warp/appservices/telegram.yaml
timeout: 30s
```
or `WARP_APPSERVICE_CONFIG_FILES=/etc/warp/appservices/telegram.yaml`.
Files are read at startup, and a file that does not parse stops the server
from starting, as a bad config file does. Adding or changing one needs a
restart. The admin API shows what is registered (`GET /appservices`) but
does not edit it.
```yaml
id: telegram
url: http://localhost:29317 # null: the service is sent nothing
as_token: <random> # what the service authenticates with
hs_token: <random> # what Warp authenticates to the service with
sender_localpart: telegrambot
rate_limited: false # absent means true
namespaces:
users:
- exclusive: true
regex: '@telegram_.*:warp\.example'
aliases:
- exclusive: true
regex: '#telegram_.*:warp\.example'
rooms: []
```
What is checked:
- `id`, `as_token`, `hs_token` and `sender_localpart` are required, and the
sender has to be a valid localpart.
- `as_token` and `hs_token` must differ. No two services may share an `id`
or an `as_token`, and no service's `as_token` may be another service's
`hs_token`.
- `url` is empty, `null` or an http(s) URL.
- Every namespace regex compiles.
Keys Warp does not know are ignored rather than refused. This is unlike
the main config file. Bridges add their own keys and unstable ones
(`push_ephemeral`, the MSC3202 keys), and a bridge upgrade must not stop
the homeserver.
The sender user is created at startup if it does not exist. It has no
password.
### How namespaces match
A namespace regex is anchored at the start and not at the end, the way
Synapse matches. Registration files in the wild are written against
Synapse. `@_irc_` matches every user ID that begins with it. `@bot` does
not match `@notabot:…`. Anchoring both ends would quietly match nothing
for many existing files.
Namespaces match full IDs: `@localpart:server` for users,
`#alias:server` for aliases, `!id:server` for rooms.
## A service acting as its users
A request carrying a service's `as_token` acts as the service's sender.
With `?user_id=` it acts as that user instead. The user has to be:
- the sender, or in one of the service's user namespaces (exclusive or
not, as the spec says)
- a user that exists — a service registers its users first
Otherwise the answer is `403 M_FORBIDDEN`. A deactivated user is
`403 M_USER_DEACTIVATED`.
Such a request has no device. `device_id` in `/whoami` is empty, and there
is no device to record activity on. A service that needs devices for its
users (for end-to-end encryption, say) logs them in with
`m.login.application_service` and uses their access tokens.
### Rate limiting
A service's own requests are never rate limited. Requests it makes as its
users are exempt when its registration says `rate_limited: false`.
Without that, a bridge puppeting a thousand users from one address would
put all of them in one bucket. The limiter runs before the token is
resolved, so the exemption is a map lookup on the token. It needs no
database query.
### Registering and logging in its users
```http
POST /_matrix/client/v3/register
Authorization: Bearer <as_token>
{"type": "m.login.application_service", "username": "telegram_12345"}
```
The user must be in the service's namespace (`400 M_EXCLUSIVE` otherwise).
There is no flow and no password, and the registration switch does not
apply: this is the service's own action, not a sign-up. `inhibit_login`
is honoured. Without it, the answer carries an access token for a new
device.
`POST /login` with `"type": "m.login.application_service"` and the
service's token logs in one of its users and returns a device. The flow
is listed by `GET /login` while any service is registered.
The service's users can change their password and deactivate their
account without the password re-authentication other users go through.
They have no password to re-enter, and the service's token is already the
strongest credential involved.
### Exclusive namespaces
A namespace marked `exclusive` belongs to its service alone:
| Who | What | Answer |
|---|---|---|
| anyone else | registering a user in it, or asking `/register/available` | `400 M_EXCLUSIVE` |
| anyone else | `PUT /directory/room/{alias}`, or `createRoom` with a `room_alias_name` that lands in it | `400 M_EXCLUSIVE` |
| the service | any of the above | allowed |
A service's sender counts as claimed by it, even outside its namespaces.
Exclusive room namespaces are not enforced. Nothing in the
Client-Server API creates a room with a chosen ID.
## Delivering events
The sender reads the roomserver's event stream from a cursor kept per
service in `appservice_state` (migration 0011). Events are not queued as
they are persisted. The stream is already durable and ordered, so a cursor
is all a service needs to catch up after being down for a day. The emit
stage only wakes the sender's workers; it hands nothing over, and it does
no work under the room lock.
One worker per service with a URL:
1. Read up to 100 events after the cursor.
2. Keep the ones that concern the service (see below).
3. If none do, move the cursor past them. Otherwise record a transaction
as in flight (its ID and the top of its events), and
`PUT /_matrix/app/v1/transactions/{txnId}` until the service accepts
it.
4. Move the cursor and clear the transaction.
A transaction that fails is retried with the same ID and the same events.
The backoff doubles from 2 seconds to 5 minutes and stays there. A service
that answers a ping has its waiting transaction sent at once.
After a restart, a transaction left in flight is sent again with its old
ID. Its events are read again from the stream range it covered, so the
service can deduplicate by ID as the spec intends. One caveat: whether
events concern the service is decided again on re-read. A room the
service's users left in the meantime can make the resent transaction
smaller than the first attempt. Within one run, retries reuse the exact
same body.
A service seen for the first time starts at the top of the stream. It is
told what happens from then on, not the server's history. Changing a
service's `id` starts it over in the same way.
### Which events concern a service
An event is delivered when any of these hold:
- its sender is one of the service's users (its sender or a user
namespace match)
- it is an `m.room.member` event whose target is one of them
- its room ID is in a room namespace
- its room has one of the service's users joined to it
- its room has one of the service's aliases pointing at it
The last two are what let a bridge see the conversations its puppets are
in. They are read once per room per batch, as the room is when the batch
is read. An event read late, from a backlog, is judged by who is in the
room then, not when the event happened.
Events are in the client format, with `room_id`, as the spec has it.
Backfilled history (negative stream positions) is never delivered.
The request carries `Authorization: Bearer <hs_token>`. The deprecated
`?access_token=` form is not sent: it was deprecated in v1.4, and a token
in a URL ends up in the service's access log. Redirects are not followed,
because they would take the `hs_token` with them.
## Asking services about users and aliases
When something needs a local user or alias that does not exist, the
services whose namespaces match are asked first. They answer by creating
it, and Warp looks again:
| When | Query |
|---|---|
| `GET /directory/room/{alias}` and `POST /join/{alias}` for an unknown local alias | `GET /_matrix/app/v1/rooms/{alias}` |
| `GET /profile/{userId}` and `POST /rooms/{id}/invite` for an unknown local user | `GET /_matrix/app/v1/users/{userId}` |
This is how a bridge creates a portal room the first time somebody looks
for it, and a puppet the first time somebody invites it. Services are
asked in registration order; the first yes ends it. A service that fails
to answer counts as a no and is logged. The request that wanted the user
goes on without it rather than failing.
## The ping
`POST /_matrix/client/v1/appservice/{appserviceId}/ping` is a service
asking Warp to ping it back. It is authenticated with the service's own
token, and `{appserviceId}` has to be that service. Warp sends
`POST /_matrix/app/v1/ping` with the `transaction_id`, and answers:
| Outcome | Answer |
|---|---|
| the service answered 2xx | `200 {"duration_ms": …}` |
| no URL registered | `400 M_URL_NOT_SET` |
| the service answered something else | `502 M_BAD_STATUS`, with its `status` and `body` |
| it could not be reached | `502 M_CONNECTION_FAILED` |
| it did not answer within `appservice.timeout` | `504 M_CONNECTION_TIMEOUT` |
| another token | `403 M_FORBIDDEN` |
## Watching delivery
`GET /_warp/admin/v1/appservices` lists each service: its namespaces,
sender and URL, never its tokens. For a service with a URL it also shows
the cursor, how far behind the top of the stream it is, the last
transaction ID, the events delivered since startup, and the failure
count, last error and next retry while it is failing. See
[admin-api.md](admin-api.md#get-appservices).
## Not implemented
| Thing | Why |
|---|---|
| Ephemeral events and to-device messages in transactions (MSC2409, MSC3202) | unstable; bridges that want them fall back to `/sync` as a user |
| Device masquerading with `?device_id=` (MSC3202) | as above; log the user in instead |
| Third-party lookups (`/thirdparty/*`) | needs the protocol metadata exchange, which no first-party client uses |
| Network-specific room directories (`PUT /directory/list/appservice/{networkId}/{roomId}`) | not served |
| Appservice key claims and queries (MSC3983, MSC3984) | unstable |
| Editing registrations at runtime | registrations are files; restart to change them |
+9 -1
View File
@@ -66,6 +66,7 @@ List-valued variables are comma-separated. A single `-` clears a list:
| `federation.tls_ca_file` | `WARP_FEDERATION_TLS_CA_FILE` | empty | Extra CA certificates (PEM) for outbound federation, on top of the system roots. |
| `media.base_path` | `WARP_MEDIA_BASE_PATH` | `./media_store` | Where media content is kept. Created on the first upload. |
| `media.max_upload_bytes` | `WARP_MEDIA_MAX_UPLOAD_BYTES` | `52428800` | The largest upload (50 MiB), and the largest file fetched from another server. Reported to clients as `m.upload.size`. |
| `media.max_user_storage_bytes` | `WARP_MEDIA_MAX_USER_STORAGE_BYTES` | `0` | The most one user may have uploaded in all. `0` = no limit. The admin API can give a user a quota of their own. |
| `media.max_thumbnail_pixels` | `WARP_MEDIA_MAX_THUMBNAIL_PIXELS` | `32000000` | The largest image, in pixels, decoded to make a thumbnail. |
| `media.thumbnail_sizes` | — | five sizes, see [media.md](media.md) | The thumbnails this server makes, as `{width, height, method}`. Empty = no thumbnails. |
| `media.pending_upload_expiry` | `WARP_MEDIA_PENDING_UPLOAD_EXPIRY` | `24h` | How long an ID reserved with `POST /media/v1/create` waits for its content. |
@@ -77,7 +78,14 @@ List-valued variables are comma-separated. A single `-` clears a list:
| `push.notification_retention` | `WARP_PUSH_NOTIFICATION_RETENTION` | `720h` | How long a read notification is kept for `GET /notifications`. Unread ones are kept until read. |
| `registration.enabled` | `WARP_REGISTRATION_ENABLED` | `false` | |
| `registration.allow_runtime_toggle` | `WARP_REGISTRATION_ALLOW_RUNTIME_TOGGLE` | `true` | |
| `registration.tokens` | `WARP_REGISTRATION_TOKENS` | `[]` | Non-empty = sign-up also needs one of these, as the `m.login.registration_token` stage. |
| `registration.tokens` | `WARP_REGISTRATION_TOKENS` | `[]` | Non-empty = sign-up also needs a token, as the `m.login.registration_token` stage. These never run out; tokens with a use limit or an expiry are made through the admin API, and both kinds are accepted. |
| `registration.require_token` | `WARP_REGISTRATION_REQUIRE_TOKEN` | `false` | Ask for a token even with no `tokens` above, for a server whose tokens all come from the admin API. |
| `appservice.config_files` | `WARP_APPSERVICE_CONFIG_FILES` | `[]` | Application service registration files, read at startup. A bad one stops the server. See [appservices.md](appservices.md). |
| `appservice.timeout` | `WARP_APPSERVICE_TIMEOUT` | `30s` | One request to an application service: a transaction, a query or a ping. |
| `server_notices.enabled` | `WARP_SERVER_NOTICES_ENABLED` | `false` | Create the notices user at startup and serve `POST /_warp/admin/v1/server_notices`. Needs a signing key. |
| `server_notices.localpart` | `WARP_SERVER_NOTICES_LOCALPART` | `server-notices` | The user notices come from. Created if it does not exist, so pick a name no real user has. |
| `server_notices.display_name` | `WARP_SERVER_NOTICES_DISPLAY_NAME` | `Server Notices` | Its display name, kept in step with this setting at startup. |
| `server_notices.room_name` | `WARP_SERVER_NOTICES_ROOM_NAME` | `Server Notices` | The name of the room each user gets their notices in. |
| `logging.level` | `WARP_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error`. |
| `logging.format` | `WARP_LOG_FORMAT` | `text` | `text` or `json`. |
| `logging.add_source` | `WARP_LOG_ADD_SOURCE` | `false` | File and line in every record. |
+1 -1
View File
@@ -250,7 +250,7 @@ not exist, `404`. A version's algorithm cannot be changed.
| Thing | Why |
|---|---|
| Dehydrated devices (MSC3814) | — |
| Claiming several keys per device (MSC3983), appservice key claims and queries (MSC3983/MSC3984) | application services are Phase 9 |
| Claiming several keys per device (MSC3983), appservice key claims and queries (MSC3983/MSC3984) | unstable; application services are in, without these ([appservices.md](appservices.md)) |
| Purging a deleted device's to-device queue | the messages stay queued; a new device later logged in under the same ID is handed them, encrypted to the old device's keys and so unreadable to it |
| Deleting a device's keys in the same transaction as the device | the two are separate stores; keys left behind by a crash between them are never served, because a query reads the device list first |
+7 -6
View File
@@ -213,7 +213,7 @@ with a message saying federation is disabled.
| | |
|---|---|
| `404 M_NOT_FOUND` | no such media, here or on its server |
| `413 M_TOO_LARGE` | larger than `media.max_upload_bytes`, uploaded or fetched |
| `413 M_TOO_LARGE` | larger than `media.max_upload_bytes`, uploaded or fetched; or over the uploader's quota |
| `504 M_NOT_YET_UPLOADED` | reserved, not uploaded within `timeout_ms` |
| `409 M_CANNOT_OVERWRITE_MEDIA` | a reserved URI that already has content |
| `403 M_FORBIDDEN` | someone else's reservation; or remote media with federation off |
@@ -227,11 +227,12 @@ with a message saying federation is disabled.
`M_UNRECOGNIZED`. Fetching arbitrary URLs for clients needs the same
network guard as media redirects plus an HTML parser, and is its own
piece of work.
- **No deletion or quotas yet.** Nothing removes media: no admin purge, no
retention for the remote cache, no per-user quota. Those are the media
parts of Phase 9's admin work, and content-addressed storage means a
purge has to check that no other row still uses a blob before deleting
it.
- **No deletion yet.** Nothing removes media: no admin purge, and no
retention for the remote cache. Content-addressed storage means a purge
has to check that no other row still uses a blob before deleting it.
Per-user quotas are in (Phase 9): `media.max_user_storage_bytes`, and a
quota per user through the admin API. An upload over the quota is
`413 M_TOO_LARGE`. See [admin-api.md](admin-api.md).
- **No quarantine**, which belongs with the same admin work.
- **No animated thumbnails**; `animated=true` gets a still image.
- **No S3** until the PocketBase driver brings its filesystem abstraction;
+1 -1
View File
@@ -174,7 +174,7 @@ authorisation rules — a knock-rule room needs an invite to join — but
`POST /knock/{roomIdOrAlias}` is not served.
**No third-party invites.** They go through an identity server, which Warp
does not talk to (Phase 9). `createRoom` with `invite_3pid`, and `/invite`
does not talk to: identity servers are a non-goal (PLAN.md section 1). `createRoom` with `invite_3pid`, and `/invite`
with `id_server`, are refused rather than silently dropped.
**No room upgrades.** `POST /rooms/{roomID}/upgrade` and the tombstone