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:
@@ -1,5 +1,5 @@
|
||||
import { config } from "./config.js";
|
||||
import { absoluteUpstream, UpstreamError, type UpstreamSession } from "./upstream.js";
|
||||
import { absoluteUpstream, hasStalwartRegistry, UpstreamError, type UpstreamSession } from "./upstream.js";
|
||||
import { generateSecret, otpauthUrl, parseOtpauthUrl, verifyTotp } from "./totp.js";
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
@@ -82,7 +82,7 @@ export async function detectBackend(sessionId: string, ctx: Ctx): Promise<Backen
|
||||
async function probeBackend(ctx: Ctx): Promise<Backend> {
|
||||
// A server with the registry answers x:AccountPassword/get; one without it
|
||||
// fails to parse the method name at all and returns unknownMethod.
|
||||
if (ctx.session.capabilities && STALWART_CAP in ctx.session.capabilities) {
|
||||
if (hasStalwartRegistry(ctx.session)) {
|
||||
try {
|
||||
const res = await jmap(ctx, [["x:AccountPassword/get", { accountId: accountId(ctx), ids: [SINGLETON] }, "p"]]);
|
||||
const [name, args] = res.methodResponses?.[0] ?? [];
|
||||
@@ -90,10 +90,14 @@ async function probeBackend(ctx: Ctx): Promise<Backend> {
|
||||
const type = (args as { type?: string } | undefined)?.type;
|
||||
if (type && type !== "unknownMethod") return "registry"; // present, but refused us
|
||||
} catch {
|
||||
// Not an answer we can read - most likely a server too old to know the
|
||||
// capability we named, which rejects the whole request rather than the
|
||||
// one call. Fall through and try the endpoint such servers do have.
|
||||
// The capability already told us this server has the registry, so a
|
||||
// request we could not read is a fault to surface, not evidence of an
|
||||
// older server. Falling back here would post the user's password to a
|
||||
// REST endpoint 0.16 removed and report the feature as unsupported.
|
||||
return "registry";
|
||||
}
|
||||
// It named the capability and then disowned the method: nothing else to try.
|
||||
return "registry";
|
||||
}
|
||||
return "legacy";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user