API reference, OpenAPI spec, and a coverage test that can fail (#51) - #99
Conversation
Three descriptions of the surface, generated from one table (shared/apiSurface.ts): docs/openapi.json, GET /api/v1/openapi.json, and a reference page at GET /api/v1/docs. Plus docs/api-reference.md as the entry point tying them to the prose that says why each rule is what it is. The interesting part is the coverage test. #51 asks that every v1 route appear in the spec, and the obvious way to check that cannot fail: a test that walks the spec's own paths and confirms each exists only finds routes documented and then deleted, never the reverse — which is the failure that actually happens. So the assertion reads the route literals out of handlePanel's OWN SOURCE and requires the table to document each one, then checks the other direction so the table cannot invent a route either. It found GET /store/admin/ledger, which had been undocumented since it shipped, on its first run. The spec is generated rather than hand-kept for the same reason: a spec beside a router drifts, and the half that drifts is always the spec, because nobody notices a document that is quietly wrong until an integrator does. The index, the spec and the docs page are the only routes served before authentication. All three are descriptions of the software — the same bytes on every install, built from a constant table — and the smoke asserts that neither the spec nor the page contains this install's server id or name. Requiring a credential would also make the docs page useless to a browser, which cannot set an Authorization header on a navigation. No CDN on the page: the panel's CSP forbids external assets, and documentation that needs the internet is a poor way to document a program that manages a server on a LAN. Asserted too. Body shapes are documented as prose in each operation's description rather than as sixty invented JSON schemas, which would be sixty more things to keep true. All twelve gates green.
…request The per-IP bucket from #50 is wired into handleSite only; on the admin listener the sole pre-auth guard is the login limiter. So the three doc routes I added were reachable with no credential and no ceiling, and the spec rebuilt a ~120 KB object graph and stringified it on every one of them - a way to spend the event loop from anywhere that can reach the port, which is the shape of thing #50 exists to stop. Both artefacts derive from a constant table, so they are built once now, and the routes spend from the same per-address bucket as the public storefront reads. Cache-Control says public, max-age=300 rather than no-store: these describe the software, and telling a client never to cache them was wrong on its own terms. Asserted with twenty consecutive fetches that must be byte-identical and average under 25ms. getPanelHtml and getPublicSiteHtml rebuild their strings per request too, and the panel page is also unauthenticated. Pre-existing and larger than this change - flagged in the PR rather than folded into it.
Self-reviewI added three unauthenticated routes to the panel listener and made two of The per-IP token bucket from #50 is wired into Two fixes, because either alone is half a fix:
Also Asserted: twenty consecutive fetches must all return the identical body and Not fixed here, and worth stating rather than leaving implied: All twelve gates green after the fix. |
**The assertion I wrote to prove the cache works could not fail.** It measured twenty requests and required an average under 25ms. So I measured the alternative instead of assuming it: | | per request | |---|---| | cached | 10.85 ms | | **uncached** | **13.30 ms** | The loopback round trip dwarfs the work by an order of magnitude. The threshold had ~12ms of headroom over the failure it was supposed to catch, so the test would have passed with the cache deleted — it asserted nothing. Same shape as the two dead assertions earlier in this run, and the reason to measure rather than pick a number that "looks safe". Replaced with a **build counter**: sixty requests across all three cached artefacts must cause **zero** rebuilds. Deterministic, machine-independent, and it fails loudly with the name of whatever rebuilt. It immediately earned that: the first run reported `rebuilt 1 artefact(s): docs`, because my baseline was taken before the docs page's *first* request, so a legitimate first build looked like a miss. Every artefact is warmed before the baseline now. A timing threshold would never have surfaced that at all. Also added: after `_resetPageCache()` the next request must rebuild **exactly once** and return the identical body — so the memo is a memo, not a one-shot that would serve an empty page if invalidation is ever added. The same dead-timing assertion was in #99 for the spec. It is removed here rather than left behind in a merged PR. ### One process note Midway through this I edited `smoke.ts` with `Get-Content | -replace | Set-Content`. PowerShell 5.1 read it as cp1254 and wrote UTF-8 with a BOM and CRLF, mojibaking every non-ASCII character in the file — including the `Ç` in the console-decoder fixture, which is a test that exists precisely because multi-byte characters break when something re-encodes them carelessly. Typecheck and build both passed; only `MSMS_SMOKE` caught it. Restored from git and reapplied through the editor. Worth recording, since the tool that broke it is the same class of bug the test was written for. All twelve gates green.
* Build the two app shells once, and limit their routes (#100) Both pages were rebuilt from their template literals on every request - a few hundred KB of string work each - and both are served before any authentication, on listeners whose only pre-auth limit was /api/login. With bindLan on that is a way to make the panel unresponsive from anywhere on the network without holding a credential. Caching an HTML page is only correct if the page cannot change, and the reason this one cannot is worth stating: every interpolation in both pages is a module constant, and everything an operator can change - theme, languages, posts, logo - is fetched by the page at runtime. So the assertion is not that the page is fast, it is that the page is not config-derived: change the site's accent and name, and the HTML must come back byte-identical. A later change that makes a page depend on config fails that, rather than this cache quietly serving yesterday's theme. Cache-Control: no-cache with an ETag, not max-age: the browser keeps its copy and asks, an unchanged shell costs a 304 with no body, and an upgrade is not followed by users pinned to a stale panel. Both page routes and the /uploads/ reads beside them now spend from the same per-address bucket as the public storefront API. Cheap is not free, and these are the only routes on either listener anyone can reach. * ## Self-review **The assertion I wrote to prove the cache works could not fail.** It measured twenty requests and required an average under 25ms. So I measured the alternative instead of assuming it: | | per request | |---|---| | cached | 10.85 ms | | **uncached** | **13.30 ms** | The loopback round trip dwarfs the work by an order of magnitude. The threshold had ~12ms of headroom over the failure it was supposed to catch, so the test would have passed with the cache deleted — it asserted nothing. Same shape as the two dead assertions earlier in this run, and the reason to measure rather than pick a number that "looks safe". Replaced with a **build counter**: sixty requests across all three cached artefacts must cause **zero** rebuilds. Deterministic, machine-independent, and it fails loudly with the name of whatever rebuilt. It immediately earned that: the first run reported `rebuilt 1 artefact(s): docs`, because my baseline was taken before the docs page's *first* request, so a legitimate first build looked like a miss. Every artefact is warmed before the baseline now. A timing threshold would never have surfaced that at all. Also added: after `_resetPageCache()` the next request must rebuild **exactly once** and return the identical body — so the memo is a memo, not a one-shot that would serve an empty page if invalidation is ever added. The same dead-timing assertion was in #99 for the spec. It is removed here rather than left behind in a merged PR. ### One process note Midway through this I edited `smoke.ts` with `Get-Content | -replace | Set-Content`. PowerShell 5.1 read it as cp1254 and wrote UTF-8 with a BOM and CRLF, mojibaking every non-ASCII character in the file — including the `Ç` in the console-decoder fixture, which is a test that exists precisely because multi-byte characters break when something re-encodes them carelessly. Typecheck and build both passed; only `MSMS_SMOKE` caught it. Restored from git and reapplied through the editor. Worth recording, since the tool that broke it is the same class of bug the test was written for. All twelve gates green.
Closes #51.
Three descriptions of the integration surface, all generated from one table
(
src/shared/apiSurface.ts):docs/openapi.jsonGET /api/v1/openapi.jsonGET /api/v1/docsdocs/api-reference.mdThe part worth reviewing: the coverage test
#51 asks that every
/api/v1route appear in the spec. The obvious way to checkthat cannot fail — a test that walks the spec's own paths and confirms each
one exists only finds routes that were documented and then deleted, never the
reverse, which is the failure that actually happens.
So the assertion reads the route literals out of
handlePanel's own source(
path === '/api/…',sub ===,action ===,rest ===) and requires thetable to document each one — then checks the other direction, so the table
cannot invent a route either.
It found
GET /servers/:id/store/admin/ledgeron its first run: undocumentedsince it shipped. That is the evidence the test is not vacuous.
Serving it unauthenticated
The index, the spec and the docs page are the only routes answered before
authentication. All three are descriptions of the software, built from a
constant table — and the smoke asserts that neither the spec nor the page
contains this install's server id or name, so "no credential" stays a
convenience rather than a disclosure. Requiring one would also make the docs page
useless to the thing that needs it most: a browser, which cannot set an
Authorizationheader on a navigation.No CDN, no Swagger bundle: the panel's CSP forbids external assets, and
documentation that needs an internet connection is a poor way to document a LAN
tool. Asserted.
Deliberate
Body shapes are prose in each operation's
description, not sixty invented JSONschemas — sixty more things to keep true, when what an integrator needs is the
method, the path, the gate, and what the call does.
Verify
All twelve gates exit 0.
MSMS_SMOKE_WEBchecks: the v1 rewrite answersidentically to the unversioned path and is anchored (
/api/v2is not v1), theserved spec is byte-identical to the generated one, coverage in both directions,
no install data in either artefact, and no external asset on the page. It also
rewrites
docs/openapi.json, so the checked-in copy cannot go stale.