Record what Administration proved on the live server

KNOWN-ISSUES carried Administration as read from source and untested. It
has now run against production: the Accounts filter was wrong (type, not
@type; fixed in #336) and everything else held -- permission casing, Basic
auth on admin calls, account and domain shapes, the zone file format, DKIM
lookup by domain, catch-all addresses, and removing a domain with its keys.
What remains unproved (an administrator-set password, and the grant-check
gap behind the outranking guard) is said plainly, along with the untranslated
invalidPatch description and the two gates that decide who may administer.

README gains Administration in its feature list and MOCK_ROLE among the mock's
switches.
This commit is contained in:
2026-09-13 16:45:18 -07:00
parent f44987e391
commit 5027bd1e73
2 changed files with 20 additions and 4 deletions
+16 -2
View File
@@ -32,9 +32,23 @@ works the same way — and dropped where 0.15 was the whole subject. Support for
0.15 was removed on 2026-08-26; the last release that runs on it is tagged
[`stalwart-0.15-support`](https://github.com/Coffey-Labs/ihasmail/releases/tag/stalwart-0.15-support).
- **Administration has not yet been exercised against a live Stalwart.** Accounts were built on 2026-09-13 against the 0.16.22 source and the mock, which reproduces the shapes read there — lists as index-keyed objects, sets as `{"id": true}`, masked secrets, AND-only filters — and it has not touched a real server. Four things are read from source rather than proved: that `/api/account` lists permissions in camelCase (`sysAccountGet`) as the enum serialises them, where the documentation shows kebab-case — both are accepted, so the menu works either way; that a new password written to `credentials/<index>/secret` is hashed and keeps the credential's id; that the Basic credential ihasmail proxies with reaches the admin `x:` methods as it already reaches the self-service ones; and that Stalwart skips its grant check when only a password changes, which is the reason the outranking guard exists at all. The last is worth reproducing rather than trusting in either direction. Query and get are sent as two requests rather than one with a back-reference, because whether the registry methods resolve references was not checked.
- **Administration was built from Stalwart's source, and the first live run found the one thing the source reading got wrong.** Accounts and Domains were written on 2026-09-13 against the 0.16.22 source and a mock reproducing it, deployed the same day, and exercised against the live server from an administrator's session. On that server the Accounts list did not load: `x:Account/query` answered **`unsupportedFilter - type`**. A registry filter is keyed by the property's name *as it appears on the object*, and the discriminator is `@type`, so `{"type": "User"}` names nothing the server knows and fails the whole query; `{"@type": "User"}` is accepted. The research that fed the build had listed the field as `type`, and the mock took it without complaint — which is how it shipped. Fixed in [#336](https://github.com/Coffey-Labs/ihasmail/pull/336), and the mock now refuses any filter name the real server does not index, answering the way Stalwart does. Everything else was **confirmed live (2026-09-13)**, mostly read-only, with the domain writes made on a throwaway domain created for the purpose and removed afterwards:
- **The Domains section is read from source as well, on the same terms.** Five things are worth confirming on a live server before trusting them: that `dnsZoneFile` is written as `name IN TYPE value` with long TXT records split into a parenthesised run, which is what `dns-update`'s BIND serialiser does on its main branch while Stalwart pins 0.5; that `catchAllAddress` takes a whole address rather than a local part; that `x:DkimSignature/query` accepts a `domainId` filter, as the server's own record builder queries by it; that a refused delete's `linkedObjects` name each object by type as `{object, id}`; and that removing a domain's DKIM keys and then the domain in one request succeeds, since automatic DKIM gives every new domain keys and a domain its keys name cannot be removed. The zone-file reader keeps any line it cannot parse as a row of its own, so a format that differs shows up as an odd row rather than a missing record.
- **Permissions** come from `GET /api/account` in camelCase (`sysAccountGet`); an administrator's list held 641 of them and none were kebab-case, whatever the documentation shows. The menu gates on these.
- **The Basic credential ihasmail proxies with reaches the admin `x:` methods**, as it already reached the self-service ones. No separate token is involved.
- **An account reads back in the shapes the code expects**: `credentials` as `{"0": {"@type": "Password", …}}`, aliases and group memberships as objects, the disk limit under `quotas.maxDiskQuota`.
- **A new domain gets automatic DKIM straight away** — an Ed25519 and an RSA key, both `active`, with their records already in the zone file — and manual DNS and certificates.
- **`dnsZoneFile` is BIND text**, one record per line as `name IN TYPE value`, with a long TXT record split into a parenthesised run of quoted chunks. A throwaway domain's file held 18 records and 3 continuation lines; every record parsed and the panel showed 18 rows. The production domains also carry TLSA records, which show as rows like any other.
- **`x:DkimSignature/query` accepts a `domainId` filter.**
- **`catchAllAddress` wants a whole address.** A bare local part is refused with `invalidPatch`, *"Invalid email address"*.
- **A domain its keys still name cannot be destroyed**: `objectIsLinked`, with `linkedObjects` listing each as `{"object": "DkimSignature", "id": …}` and no description. Removing through the panel destroys the keys first and then the domain; both were gone afterwards.
- **A reserved TLD is refused**: `example` as a domain's top level comes back `invalidPatch`, *"Invalid domain name"*, naming `name`.
Two things remain unproved, and both need something this session could not do. **A password set by an administrator** is written to the account's existing credential (`credentials/<index>/secret`); the source says it is hashed and keeps the credential's id, which matters because OAuth tokens are tied to it, but signing in with the new password has not been tried. **The outranking guard** exists because the source shows Stalwart skipping its grant check when only a password changes and on a delete; confirming that needs a session with a role below administrator looking at one above it. Until both are tried, trust the guard rather than the gap.
- **Some refusals show Stalwart's own English description.** The server's `invalidPatch` — used for a reserved TLD and a malformed catch-all address, among others — is not one of the error types the client explains in its own words, so what appears is the server's text, untranslated. Nothing is lost: the description says what is wrong. It is simply English in a German interface.
- **Administration is off for a device not marked as your own, and for an installation that says so.** Both are enforced by the server rather than hidden by the menu: such a session is sent no permissions, and the JMAP proxy refuses registry methods beyond the account's own. That is worth stating because the proxy otherwise forwards whatever the browser sends, and before these gates an administrator's console could make any registry call their role allowed. For a session that may not administer, the proxy reads a request body only when it could name a registry method — a `"x:` in the text, or a `\u` escape that could spell one — so ordinary mail traffic is forwarded untouched.
- **All nine translations have never been read by anybody who speaks them.** They were produced by AI against standard dictionaries on 2026-08-31 — German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, which with English makes ten languages in the picker — and every one of the nine is marked **Beta** in the picker, with that stated in Settings beside a link for reporting anything that reads wrongly. This is the entry that matters most on this page, because it is the one thing here that cannot be closed by testing: a translation can be complete, consistent, pass every check, and still read like a machine wrote it, and nobody on this project can tell which. What *is* verified is the machinery around them. A missing key renders its English source, so a bad line can simply be deleted; a stale key — one whose English no longer exists — is caught by `npm run i18n:check` rather than sitting in the file looking correct and never being looked up. Plurals are asked of `Intl.PluralRules` rather than assumed, which is why Russian and Ukrainian carry three forms and Japanese and Chinese carry one; supplying `one` for Japanese would have been filling in a distinction the language does not draw. Confirmed live on the deployed instance (2026-08-31) against a 6,289-message mailbox: role folders localise and the ~20 custom folders keep the names their owner gave them, dates and the calendar follow the language, and 6,289 renders as *6289 листувань* — the genitive plural a number ending in nine takes, which is the first time the plural machinery ran on anything but a hand-picked value.