chore(server): freeze the URL surface with a generated route manifest (PR 0) #99
Reference in New Issue
Block a user
No description provided.
Delete Branch "chore/route-manifest"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
PR 0 of the router domain split —
docs/website/API_V2_PLAN.md§ Phase 2. No router file moves in this PR.The split promises that
admin.routes.js(1552 lines, 110 routes) can be carved into one router file per business capability with every URL unchanged. That promise has to be provable by a diff rather than asserted in review, otherwise each later PR is "trust me, it's mechanical". This lands the tool that proves it.server/scripts/routeManifest.js(npm run routes:manifest) walks the live Express stack and writes a sorted{ method, path }list toserver/routes.manifest.json.admin.routes.jssit on the line afteradminRouter.get(, which defeats naive greps.swagger-output.json. That is annotation-derived (an unannotated route is invisible) and churns when a description is reworded — it documents intent. The manifest records reality./api/**+/.well-known/**plus the internal listener. The SPA catch-all,/uploadsand/brandare filesystem-conditional static mounts; including them would make the output depend on whether CI had built the client. Static mounts are not API contract.The generator reproduced the frozen baseline in
docs/website/api-route-inventory.jsonbyte-for-byte on its first run — 199 public routes + 2 internal. That was PR 0's own acceptance test, and it means the committed baseline is confirmed accurate rather than merely claimed.CI now runs
npm run routes:manifest -- --checkon every PR, so a URL change can only merge by deliberately committing the new manifest — putting it in front of a reviewer instead of letting it ride along in a "mechanical" refactor.Also:
routes.guards.json— a review aid, explicitly not a contractPer 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 its gate would publish authenticated endpoints silently, and the manifest (method + path only) would show nothing.Names are a hint only:
requireRole(...)returns an anonymous arrow and cannot be observed. But a vanishedrequireAuthis unambiguous, so the test suite asserts it directly from the introspected stack for all 130/admin/**and/player/**routes.App-level plumbing (helmet, morgan, the JSON parser, the bot guard) is dropped from the output — it applies uniformly to all 199 routes and would bury the per-route gates.
Deviation from the plan: the unauthenticated-status snapshot was tried and dropped
The plan offered an optional stronger check — fire an unauthenticated request at every manifest path and snapshot the status code, catching a dropped
adminOnlyas403 → 200in a way names cannot — and said to drop it rather than ship a flaky gate.Dropped. Against the dead-port mariadb pool the test suite uses, DB-touching routes do not fail fast; they sit on the pool's acquire timeout. A partial sweep had not finished after two minutes. A flaky two-minute gate is worse than none. The
requireAuthassertion above covers the same class of regression deterministically and runs in ~1 s.Docs companion: RunicGateway/docs#49.
How it was tested
Check mode was verified in both directions: hand-editing
/api/health→/api/healthzin the committed manifest makes it exit 1 withstale: routes.manifest.json; reverting returns exit 0.No route was added, changed or removed, so
npm run swaggerproduces no diff and the spec is untouched.Checklist
AI-assisted contributions (required)
Claude Code (Opus). I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-By/Assisted-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.
PR 0 of the router domain split (docs/website/API_V2_PLAN.md § Phase 2). The split promises that admin.routes.js can be carved into one router file per business capability without moving a single URL. That promise has to be proved by a diff, not asserted in review — this lands the tool that proves it, with no router file moved. scripts/routeManifest.js walks the live Express stack (runtime introspection, not source parsing: route paths in admin.routes.js sit on the line *after* `adminRouter.get(`, which defeats greps) and writes a sorted { method, path } list to routes.manifest.json. It reproduces the frozen baseline in docs/website/api-route-inventory.json byte-for-byte — 199 public routes plus 2 on the internal listener — so the freeze is confirmed accurate, not just claimed. Scope is /api/** and /.well-known/** plus the internal app. The SPA catch-all, /uploads and /brand are filesystem-conditional static mounts, so including them would make the output depend on whether CI had built the client. Static mounts are not API contract. Also emits routes.guards.json — a review aid, not a contract: per route, the handler count and the *named* middleware on its mount chain. Router-level `use(noindex, isLoggedIn, staffOnly)` gates never appear in an individual route's own stack, so an extracted capability router that forgot to re-apply one would otherwise publish authenticated endpoints silently. Names are a hint only (requireRole(...) returns an anonymous arrow), but a vanished requireAuth is unambiguous — and the test suite asserts every /admin/** and /player/** route still carries it. The plan's optional unauthenticated-status snapshot was tried and dropped, as it allowed: against the dead-port mariadb pool the tests use, the sweep sits on the pool's acquire timeout and had not finished after two minutes. A flaky two-minute gate is worse than none; the requireAuth assertion covers the same regression deterministically. CI runs `npm run routes:manifest -- --check` on every PR, so a URL change can only merge by deliberately committing the new manifest. Co-Authored-By: Claude <noreply@anthropic.com>