Mints a single-use, passwordless registration token for each guest,
pinned to their address and username and expiring after
`guest_invites.invite_lifetime`, and schedules their invite emails. The
links are only ever mailed to the guests, so the response carries a
count rather than the tokens.
Everything is validated before anything is minted, and the batch is
committed as a whole, so a rejected request sends no email at all. Each
invite goes through the registration policy as `POST /register` will
check it, so an invite the guest form would refuse is a 400 naming the
invite and every violation. The policy sees an empty requester, as the
request comes from the inviter's homeserver rather than the guest. A
username that is already taken is refused up front, rather than emailed
out as a link nobody can register with. Addresses are deduplicated
ignoring case before the usernames are checked, keeping the first entry,
so a repeated address never counts as a username given twice.
The caller sends the room name and the inviter on the inviter's behalf,
so the endpoint doesn't trust them. The room name and the inviter's
display name are stripped of control characters, including the
bidirectional controls `char::is_control` misses, and truncated before
they are stored in the job payload. The inviter's Matrix ID is never
truncated, so it can't show a server it isn't from: it is kept whole if
it is valid under the historical grammar, and left out otherwise.
The room ID is checked as `/open-room` checks it, and is only recorded
on the span once it is valid.
The endpoint answers 404 while guest invites are disabled. The check is
in the handler, as the admin router doesn't see the configuration.
`guest_invites.enabled` turns it on, and requires `client_room_url`.
`/open-room` keeps using `client_room_url` while the endpoint is
disabled, so that links already sent keep working.
`invite_lifetime` is a positive number of seconds that fits in a `u32`,
so MAS refuses a zero or negative lifetime at startup, and adding it to
the current time can't overflow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A registration token can now be marked `passwordless`, which will let the
user register without picking a password: their identity is established by
the email verification code instead.
This only adds the column and plumbs it through the storage layer and the
admin API; the registration flow itself is unchanged for now.
Registration tokens now report the `/register?token=…` deep link alongside the
`username` and `email` they were issued for, and the create endpoint accepts
both. All the token endpoints now take the `UrlBuilder` so they can build that
link, and `Register` carries the token in its query string.
A registration token can now optionally carry the username and/or the email
address the account it is used for has to be registered with. This only adds
the columns and plumbs them through the storage layer and the CLI; nothing
enforces them yet.
Mechanical output changes from the dependency bump:
- aide 0.16 strips redundant null branches from optional query-param schemas
(optionality stays expressed via required:false on the parameter).
- schemars 1.0 changes doc-comment dedenting, emits the ClientsConfig newtype as
a named definition, and simplifies generic schema names
(e.g. JsonWebKeySet_for_JsonWebKeyPublicParameters -> JsonWebKeySet).
Make `filter[client]` on `GET /api/admin/v1/oauth2-sessions` repeatable
so admin tooling can fetch sessions belonging to a set of clients in
one request. The field on `FilterParams` changes from `Option<Ulid>`
to `Vec<Ulid>`; the struct was already extracted with
`axum_extra::extract::Query` so the repeated values are not silently
dropped.
Each client ULID is validated to exist (mirroring the previous
single-client `404` behaviour) before being passed to the new
`OAuth2SessionFilter::for_clients` storage filter. The `Display`
impl used to reconstruct cursor links now emits one
`filter[client]=…` segment per client so pagination preserves the
filter. The OpenAPI schema is regenerated via `misc/update.sh` and
now describes the parameter as an array.
Adds `filter[has-active-compat-session]=true|false` to
`GET /api/admin/v1/users`. When `true` (resp. `false`), only users with
(resp. without) at least one active (non-finished) compatibility session
are returned.
Adds `filter[active-oauth2-client]` to `GET /api/admin/v1/users`. The
filter is a repeatable ULID query parameter; the semantics are OR across
the supplied clients (a user matches if they have an active OAuth2
session belonging to any of them).
Each supplied client ID is validated up front: a missing client returns
404 (mirroring the pattern in `oauth2-sessions/list`).