Look for Stalwart's capability where Stalwart advertises it

Self-service credentials, the About page and Files all keyed off
`urn:stalwart:jmap`, and all three looked for it in the session-level
`capabilities`. Stalwart has never put it there. `Session::new` builds that
list from a fixed set the capability is not part of, in any 0.16.x from
0.16.0 to 0.16.19; it is handed out per-account instead, so it arrives in
`primaryAccounts` and in each account's `accountCapabilities`.

So every real 0.16 server read as pre-0.16. Password changes, 2FA and app
passwords fell back to `POST /api/account/auth`, which 0.16 removed, and
reported that the server offers no self-service credential management. About
named the wrong generation. Files ran the pre-0.16 path, omitting `nodeType`
and listing the tree through get.

Look in all three places, on both sides. Two nearby soft spots go with it: a
transport error while probing the registry no longer downgrades a server to
the legacy path -- which would have posted the current password to an
endpoint that is not there -- and a locale request that is merely refused no
longer discards a generation the capability had already settled.

The mock advertised the capability in the session, which is why no test ever
caught this; it now advertises it where the real server does, and validates
`using` by the urn rather than by the session, as Stalwart does. Put the old
lookup back and nine tests fail.

Stalwart still publishes no version number to clients -- VERSION_PUBLIC is a
fixed "1.0.0" -- so About continues to report the generation and edition,
which are now the right ones.
This commit is contained in:
2026-08-24 22:21:26 -07:00
parent 03b5a6c388
commit 14125a0799
9 changed files with 203 additions and 28 deletions
+15
View File
@@ -46,6 +46,21 @@ after(() => {
(mock as { server?: { close(): void } }).server?.close();
});
/**
* What the About page reads. Stalwart advertises `urn:stalwart:jmap` only
* per-account, so a session that looks for it at the top level reports a real
* 0.16 server as older than 0.16 — the same mistake that sent credentials to
* the removed REST endpoint.
*/
test("the session reports the 0.16 generation the server actually is", async () => {
const res = await call("/api/auth/session");
assert.equal(res.status, 200);
assert.equal(res.body.ihasmail.server.generation, "0.16+");
assert.equal(res.body.ihasmail.server.edition, "oss");
assert.equal(res.body.capabilities["urn:stalwart:jmap"], undefined, "not where a client would first look");
assert.ok("urn:stalwart:jmap" in res.body.primaryAccounts, "but here, as on a real server");
});
test("the 0.16 registry backend is detected and reported empty", async () => {
const res = await call("/api/account/security");
assert.equal(res.status, 200);