docs(website): record the landed route manifest and resequence CSP after PR 0

Companion to website PR 0 (chore(server): freeze the URL surface with a
generated route manifest).

BACKEND_DESIGN.md gains § 4.0, naming the two generated artifacts that are
actually authoritative about the API and what each is authoritative *for*: the
manifest records which URLs exist (introspection-derived, reality), the Swagger
spec records what they mean (annotation-derived, intent). The prose tables in
§ 4 are orientation and can drift; those two files cannot. Also documents
routes.guards.json as a review aid that is explicitly not a contract.

API_V2_PLAN.md marks PR 0 shipped and records its two deviations. The optional
unauthenticated-status snapshot was tried and dropped exactly as that section
allowed — against the dead-port mariadb pool the tests use it sits on the acquire
timeout rather than failing fast — replaced by a deterministic assertion that
every /admin/** and /player/** route still carries requireAuth.
routes.guards.json is committed and staleness-checked even though a diff in it
is not a contract change, because an ungenerated review aid rots into a
misleading one.

The sequencing section is corrected: PR 0 now runs before the CSP pair. The CSP
report-only PR must stand up a POST /api/csp-report collector for `report-to` to
target, which is a new URL under /api/**; landing it first would have left PR 0
generating 200 routes against a 199-route baseline, destroying its own acceptance
test. With PR 0 first, the collector appears as a reviewed, deliberate +1 in the
manifest — the mechanism working as intended.

api-route-inventory.json is unchanged, which is the point: the generator
reproduced it byte-for-byte on first run.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-27 14:55:57 -05:00
parent 40cd9375d7
commit b1a474a7eb
2 changed files with 69 additions and 4 deletions

View File

@@ -33,6 +33,12 @@ Skeleton from the spec, with a small number of justified additions marked **(+)*
> 110 routes) are being split into one router file per business capability — **in place, with every
> URL unchanged**. This section and §4 get updated as each split PR lands. See
> [API_V2_PLAN.md](./API_V2_PLAN.md) § Phase 2.
>
> "Every URL unchanged" is enforced mechanically, not by review: `server/scripts/routeManifest.js`
> (`npm run routes:manifest`) walks the live Express stack and writes the sorted
> `{ method, path }` freeze to `server/routes.manifest.json`, mirrored here as
> [api-route-inventory.json](./api-route-inventory.json). PR checks regenerate it and fail on any
> diff, so a split PR that moves a URL cannot merge silently. See § 4.0.
```
server/
@@ -257,6 +263,34 @@ marker. Cleared wholesale on TOTP disable / password change / password reset.
Base path `/api/v1`. JSON in/out. Auth via httpOnly cookie (`isLoggedIn` reads it; also
accepts `Authorization: Bearer` for API testing).
### 4.0 The authoritative route list
The prose tables below are **orientation for a human reader** and can drift. Two generated artifacts
are authoritative, and they answer different questions:
| Artifact | Source of truth for | Generated by |
|---|---|---|
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs exist.** 199 public routes + 2 on the internal listener, sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
| `server/swagger/swagger-output.json` — served at `/api/docs` | **What each route means.** Parameters, bodies, response codes, security. | `npm run swagger`, from `#swagger.*` annotations |
The split is deliberate: Swagger is annotation-derived, so an unannotated route is invisible in it and
it churns whenever a description is reworded — it documents *intent*. The manifest is introspection-
derived and records *reality*, which is why it, not Swagger, is the thing PR checks freeze
(`npm run routes:manifest -- --check`).
Scope: the manifest keeps `/api/**` and `/.well-known/**` from the public app plus everything on the
internal listener. The SPA catch-all, `/uploads` and `/brand` are filesystem-conditional static
mounts — not API contract, and including them would make the output depend on whether CI had built
the client.
A third generated file, `server/routes.guards.json`, is a **review aid and not a contract**: per route,
the middleware handler count plus the *named* middleware on its mount chain. It exists because a
router-level `router.use(noindex, isLoggedIn, staffOnly)` gate never appears in an individual route's
own stack, so a capability router extracted without re-applying the gate would otherwise publish
authenticated endpoints silently. Names are a hint only — `requireRole(...)` returns an anonymous
arrow and cannot be observed — but a *missing* `requireAuth` is unambiguous, and the server test suite
asserts every `/admin/**` and `/player/**` route still carries it.
### /auth (auth.routes.js → auth.controller.js)
| Method | Path | Auth | Body | Purpose |
|---|---|---|---|---|