`POST /api/admin/v1/room-invites` minted a token for each address and
emailed it in one call. It becomes
`POST /api/admin/v1/user-registration-tokens/invite-emails`, which
emails the links of tokens minted through the token API: to the token's
own address, or to one given for a token without an address. A token
without a room can be sent too.
The `room_invites.enabled` and `room_invites.invite_lifetime` settings
are gone: minting is the token API's, which takes an expiry, and every
admin API client may already read invite links.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R27QDzJTWSBxkrfbxBxcKF
A pinned email must parse and a username template must have one
`{random}`. Both must pass the registration policy, and a pinned
username which isn't a template must not be taken, so that a token
isn't minted that nobody can register with.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R27QDzJTWSBxkrfbxBxcKF
An invite's `username` may be a template with one `{random}`, which the
minted token stores as is and registering fills in. The endpoint checks
a template by filling `{random}` with letters and digits and running the
registration policy on the result, so a template the policy refuses is a
400 when inviting rather than an invite that can't register. A template
with more than one `{random}` is a 400. Every invite may carry the same
template, and the name it fills in to is only known when registering, so
the repeated-username and username-taken checks apply to literal
usernames only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RkxQWj8RAKd7iZ1JQtXdxm
Nothing in the endpoint, its email or its config is specific to who is
invited, so they take neutral names: `POST /api/admin/v1/room-invites`
with the `room-invite` OpenAPI tag, `SendRoomInviteEmailJob` on the
`send-room-invite-email` queue, the `emails/room_invite.*` templates and
their `mas.emails.room_invite.*` strings, and the `room_invites_enabled`
and `room_invite_lifetime` site config fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RkxQWj8RAKd7iZ1JQtXdxm
`SendGuestInviteEmailJob` sends one guest invite email for a
registration token, so that a bad address or a transient SMTP failure
retries that one email rather than a whole batch. It takes both the
recipient and the invite link from the token, so the link can't be sent
to anyone but the address the token is pinned to. A token revoked,
expired or used by the time the job runs gets no email.
The link registers with the token, then opens the room through the
`OpenRoom` post-auth action. A browser already signed in to MAS goes
straight to the room.
The email names the room and who sent the invite, when the job has
them, and tells the recipient they are asking to join, since someone in
the room still has to approve them. Each combination has a message of
its own, rather than one message passed into another, which would
escape the names twice in HTML. Its subject names neither, as both are
chosen by the sender.
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>
Claude-Session: https://claude.ai/code/session_01R27QDzJTWSBxkrfbxBxcKF
A token's `username` with exactly one `{random}` is a template: it
allows any username with its literal parts and 32 lowercase letters and
digits in place of `{random}`. Localparts can't contain `{`, so no
existing username is read as one. Any other value is imposed as is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RkxQWj8RAKd7iZ1JQtXdxm
Holds the settings for inviting guests to rooms by email, starting with
the client URL invitees are sent to once they are signed in. The value
goes on `SiteConfig`, which the admin handlers and the views already
take.
`client_room_url` is a string rather than a URL, because parsing it as
one would percent-encode the braces of a `{room_id}` placeholder in its
path, which would then never be replaced. Whenever it is set, it must
contain `{room_id}` and still parse as an absolute `http` or `https` URL
with a host once the placeholder is filled in, so that `/open-room`
never redirects relative to MAS.
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.
Exposes a new query telling whether a given username is available for
registration, and if not, why: already taken, or rejected by the
registration policy. A username reserved on the homeserver side is
deliberately reported as taken, mirroring the ambiguity of the
registration form, which doesn't tell the two apart either.
Because the query reveals whether a username exists, it is rate-limited
per requester, configurable through the new
`rate_limiting.username_availability` option.
Add a dedicated `device_code_link` rate limiter for device authorization user code verification.
This is to help mitigate the User Code Brute Forcing attack as described by [RFC 8628 section 5.1](https://datatracker.ietf.org/doc/html/rfc8628#section-5.1)
Proposed default is 1 per minute and burst of 10. This can be overridden by the server admin.
`pnpm/setup` reads the runtime version from `package.json` and ignores `.node-version`. With `onFail: download`, pnpm records Node 24.15.0 in the lockfile and downloads it on every install unless `--no-runtime` is passed. The Dockerfile and the docs build pass it, since they already run a pinned Node. `.node-version` stays for the Cloudflare Pages build, which reads it to pick the Node that bootstraps corepack.
The page said encrypted bridges cannot work with MAS and that a solution
was being worked on. That solution is MSC4190, stable since Matrix 1.17
and enabled for every application service in Synapse 1.141.0, so the
limitation no longer exists.
Replace the page with a readiness item on the migration page, which is
where the original warning was aimed, and redirect the old URL there.
Matrix 1.17 requires servers that don't support the legacy
authentication API to reject `m.login.application_service` on
`/login` with a 400 and the `M_APPSERVICE_LOGIN_UNSUPPORTED` error
code. Previously this login type fell through the generic unsupported
credentials path and returned `M_UNKNOWN`, which appservices written
against Matrix 1.17 can't distinguish from an unrelated failure.
Until now only the clippy CI job and the Dockerfile named a Rust version (kept in sync by hand), while every other CI job, the release binaries and the docs build ran on whatever `stable` happened to be that day. Rust 1.98.0 landing on 2026-08-20 broke `build-binaries` overnight because of that (#5935).
`rust-toolchain.toml` pins 1.96.0 with the `minimal` profile plus clippy, and every `rustup toolchain install stable ...` in CI becomes a bare `rustup toolchain install`, which reads the file. Starting at 1.96.0 (the version clippy is already clean against) keeps this change free of lint churn; catching up to 1.98.0 is a follow-up.
rustfmt stays on nightly because `.rustfmt.toml` uses nightly-only options, so that job now invokes `cargo +nightly fmt` explicitly instead of setting a rustup directory override, which would silently take precedence over the toolchain file.
The file does not list the linux cross-compilation targets on purpose: that would make every developer and CI job download `rust-std` they never use. The two consumers that cross-compile add the targets themselves.
The interactive playground page and its route/config option are gone; the GraphQL API itself (POST /graphql) is unaffected. External GraphQL usage is deprecated anyway in favour of the REST admin API.
Fixes#5572Fixes#2424
This makes it so that MAS binds on a temporary path on startup, sets
permissions and then moves it to the configured path. This means that we don't
error out anymore if there is a stale socket file, and that it is safe to
rollout a new process without killing the old one first, without breaking
connections.
When a browser signs out (or discovers its session was ended), the
session cookie now remembers when that happened; the flag is cleared by
the next successful login. The `additional_authorization_parameters`
templates can read it as `logged_out`, so operators can force a fresh
prompt at the upstream provider after sign-out, e.g.:
additional_authorization_parameters:
prompt: "{% if logged_out %}login{% endif %}"
This addresses the "sign out signs me back in" loop with upstream SSO
providers that still hold a live session.
Fixes#1569
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).
branding.logo_uri was accepted and validated by the config layer but never
plumbed through to SiteConfig or any template, so it had no effect. Remove the
field and regenerate the committed config schema to remove the confusion.
This was done by auditing what's in the `mas-config` crate (and resulting
config JSON schema) and comparing it to the current configuration options
documentation.
Add a reference page describing the `http.server.response` log line and its
fields (method, status, requester, client address, GraphQL operation, request
id, trace id, …), aimed at administrators feeding the logs into a SIEM, with a
table of well-known operations and how to classify them (login, logout, token
issuance, …).