dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 1 | # macltok request, keep Matrix protocol knowledge out of hosts |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 2 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 3 | ## Problem |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 4 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 5 | malice should know how to represent Matrix things (show a homeserver field, print ids as strings) |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 6 | and nothing more. Today it carries protocol knowledge macltok should own, and some of it is written |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 7 | twice, once in `malice-server` and once in `malice-worker`. Every other macltok host would have to |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 8 | write the same code. Five requests, ordered by impact. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 9 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 10 | ## 1. Avatars as `MediaId`, not MXC URIs |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 11 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 12 | `RoomSummary::avatar` and the other `avatar` fields in `model.rs` are `Option<OwnedMxcUri>`, so a host |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 13 | fetches them with `ClientHandle::mxc_media(&MxcUri, MediaSize)`. Attachments go through the opaque |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 14 | `MediaId` and `ClientHandle::media`. Because of that split, malice validates raw `mxc://` strings |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 15 | with `ruma::MxcUri::is_valid` and sends MXC URIs over its HTTP API to the browser and back. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 16 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 17 | Request, register avatars like attachments and expose them as `Option<MediaId>`, or as |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 18 | `MediaDescriptor` if the mimetype or size is known. Then hosts fetch every image through |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 19 | `media(&MediaId, MediaSize)`. `mxc_media` can stay for hosts that want it, but no model field |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 20 | should require it. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 21 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 22 | `set_avatar(Option<OwnedMxcUri>)` is fine as it is if its input comes from an upload outcome |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 23 | and not from the host. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 24 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 25 | ## 2. Soft-logout re-login decision |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 26 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 27 | malice runs the same flow in `malice-server/src/state.rs` (`login`) and |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 28 | `malice-worker/src/lib.rs` (`Host::login`). It restores the store, and on |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 29 | `Restore::SoftLoggedOut(soft)` checks whether the submitted username is that user. If so it calls |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 30 | `soft.password_login(password)` to keep the device and its keys. Otherwise it does a fresh |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 31 | `PasswordLogin::new(..)`. The username check is copied in both crates. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 32 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 33 | ```rust |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 34 | fn username_matches(submitted: &str, user_id: &ruma::OwnedUserId) -> bool { |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 35 | let submitted = submitted.trim(); |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 36 | submitted == user_id.as_str() || submitted == user_id.localpart() |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 37 | } |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 38 | ``` |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 39 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 40 | That check is Matrix knowledge (a localpart or a full MXID names the same account), and it is |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 41 | only correct when the homeserver also matches. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 42 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 43 | Request, either shape works. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 44 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 45 | ```rust |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 46 | impl SoftLogout { |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 47 | /// whether a login form's homeserver and username name this soft-logged-out account |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 48 | pub fn matches(&self, homeserver: &str, username: &str) -> bool; |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 49 | } |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 50 | ``` |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 51 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 52 | Or have `Session::login(PasswordLogin, sessions)` resume a soft-logged-out record in `sessions` |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 53 | by itself when it names the same account, so hosts never branch on it. The second shape is |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 54 | preferred because both malice call sites would shrink to one call. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 55 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 56 | ## 3. Homeserver input validation, and a discovery bug it causes |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 57 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 58 | macltok builds the client with `server_name_or_homeserver_url(login.homeserver.trim())`, which |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 59 | does `.well-known` discovery for a bare server name. malice runs `normalise_homeserver` |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 60 | (`malice-core/src/login.rs`) first, and that prefixes `https://` onto anything without a scheme. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 61 | So a user typing `example.org`, whose homeserver is `matrix.example.org`, gets |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 62 | `https://example.org` treated as a homeserver URL and discovery never runs. malice will drop its |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 63 | normaliser once macltok covers what it checks. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 64 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 65 | - reject empty input and input containing whitespace |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 66 | - reject schemes other than http and https |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 67 | - reject a userinfo part (`user@host`) and a missing host |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 68 | - reject plain http unless the host opts in, and even then only for loopback hosts (`localhost`, |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 69 | `127.0.0.1`, `::1`) |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 70 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 71 | Suggested shape is a builder option plus a typed error. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 72 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 73 | ```rust |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 74 | PasswordLogin::new(homeserver, username, password) |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 75 | .allow_insecure_loopback(true) |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 76 | ``` |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 77 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 78 | with a distinct `AccountError` variant (or `ErrorKind`) for invalid homeserver input, so a host can |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 79 | answer 400 with the message instead of a generic login failure. The same validation should apply |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 80 | to the OAuth login path. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 81 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 82 | ## 4. `AccountError::Operation` hides the homeserver's answer |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 83 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 84 | `Operation` displays as `"{context} failed"` and the useful part (the homeserver's errcode and |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 85 | message) is only in `source()`. malice has a `describe_error` helper that walks the source chain to |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 86 | build a readable message. Every host would need it. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 87 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 88 | Request, include the innermost meaningful cause in `Display`, or add an accessor such as |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 89 | `AccountError::user_message() -> String` that renders the chain. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 90 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 91 | ## 5. Re-export `ruma` |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 92 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 93 | macltok's public API is typed with ruma (`OwnedRoomId`, `OwnedEventId`, `OwnedUserId`, |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 94 | `OwnedRoomOrAliasId`, `OwnedMxcUri`), so hosts must depend on ruma at exactly macltok's version. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 95 | malice pins `ruma = "0.16"` in two places to match. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 96 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 97 | Request, `pub use ruma;` in `lib.rs`, same as `imbl`. malice will then parse ids through |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 98 | `macltok::ruma` and drop its own ruma dependency. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 99 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 100 | ## Not needed |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 101 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 102 | `VerificationAction` already derives `Deserialize` under the `serde` feature, so malice will drop its |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 103 | hand-written string match on its own. |