refactor(server): split public, player and residual auth into capability routers (PR 5) #106
Reference in New Issue
Block a user
No description provided.
Delete Branch "refactor/router-split-5"
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
The last split PR of docs/website/API_V2_PLAN.md § Phase 2 — the domain split is complete.
public.routes.js,player.routes.jsandauth.routes.jsare deleted. Each group is now a directory whoseindex.jsowns the group gate and the mount table and declares no routes of its own, matchingadmin/. Every one of the 200 routes in the manifest is now declared in a capability router; no monolithic route file remains inrouter/v1/.publicposts.router.js/public/postssiteModeper routewiki.router.js/public/wikisiteModeper routepages.router.js/public/pagessiteModeper route except the previewshard.router.js/public/shardsiteModegatedsite.router.js/settings/status/version/contact)playeraccount.router.js/player/accountshard.router.js/player/shardappeals.router.js/player/appealsauthlogin.router.js/auth/loginloginGuardsper routeregister.router.js/auth/registerloginGuards+registerLimiterinvite.router.js/auth/inviteloginGuards+registerLimiteron acceptpassword.router.js/auth/passwordsession.router.js/logout/me)24 + 20 + 10 = 54.
auth/'s other 32 routes (me23,mobile5,sso4) were already in their own files and did not move.No URL moves. Docs PR: RunicGateway/docs#60.
The parts that are not mechanical — please look here
Two of the three groups have no group gate, and that is the security-relevant fact about them.
player/index.jscarriesnoindex, requireAuth— authenticated, any role, because staff are a superset of players.public/index.jsandauth/index.jscarry nothing, deliberately: the public surface is anonymous by contract (logged-out SPA, Discord bot, and the AndroidShardStreamClienton/public/shard/stream, none of which send credentials), and/authis where an anonymous caller becomes authenticated. Both index files say so, because the obvious "hardening" edit to either one is an outage.GET /auth/mehas a mount-order dependency.authRouter.use('/me', meRouter)matches the bare path/me, not just/me/*— so the request runsmeRouter's (andnotifRouter's)noindex, requireAuth, matches no route inside either, and falls through to its own handler.session.router.jsmust therefore stay mounted last. Verified by the counterfactual rather than by reading the mount table: moving the mount to the top ofauth/index.jsstill answers401, but the response loses itsX-Robots-Tagheader. Neither manifest can see that; only a header assertion can.Two root-mounted routers, on the PR 4
dashboard.router.jsprecedent:public/site.router.jsandauth/session.router.jshold the routes owning no path segment. Safe at the root only because neither declares router-level middleware — a bareuse(gate)there runs for every request passing through toward another mount. Both files carry that note.loginGuardsis the PR's one shared module, the counterpart to PR 3'simageUpload.js. The[backoffGuard, slowLogin, loginLimiter]array was inline inauth.routes.js, spread by four routes this PR puts in three files — plus a fifth, already-duplicated copy insso.routes.js. Now one definition inauth/loginGuards.js, imported by all five, exportedObject.freezed (it is module-level shared state; a router that pushed onto it would silently add middleware to every other login surface).Deviations from the plan's target tree
session.router.jsis new — the tree listedlogin.router.jsbut had nowhere for/logoutandGET /me, which own no prefix. Folding them intologin.router.jswould have forced that router to the group root and given up prefix ownership for the four login routes.posts.router.js, notnews.router.js— matches the/postsprefix it owns and itsadmin/posts.router.jssibling.sso.routes.js/mobile.routes.jswere not renamed to*.router.js. They did not move in this PR; churning their names would add diff noise to a PR whose value is being reviewable.public.controller.jswas not split. It still serves settings/status/version/contact and posts/wiki/pages. These PRs re-wire routes, not logic; splitting a controller is a separate change with a separate risk profile.How it was tested
All four gates zero-diff (none of these files appears in the commit):
server/routes.manifest.json— 200 public + 2 internalserver/routes.guards.json— no route lost or gained a gateserver/swagger/swagger-output.json— 198 operations, byte-identicaldocs/website/api-route-inventory.json— already in syncTwo checks the manifest provably cannot perform, both run explicitly:
:paramshadowing — the manifest sorts its entries, so a declaration reordering is invisible in all four gates. Walked the built stack in dispatch order: 86 public/player/auth routes, 64 of them literal, none shadowed. The only ordering-sensitive pair isGET /public/wiki/{categories,tags}ahead of/public/wiki/:slug(the public twin of theadmin/wiki.router.jstrap PR 3 found);/public/pages/:id/preview/:tokenalso stays ahead of/:slug, at a different depth./auth/meheader assertion described above — confirmedX-Robots-Tag: noindex, nofollowis still present on the split, matchingmain, and confirmed it disappears ifsession.router.jsis mounted first.Checklist
AI-assisted contributions (required)
Claude Code (Opus 5). 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.
The last split PR of docs/website/API_V2_PLAN.md § Phase 2. public.routes.js, player.routes.js and auth.routes.js are deleted; each group is now a directory whose index.js owns the group gate and the mount table and declares no routes. Every one of the 200 manifest routes is now in a capability router. public/ posts (2) wiki (4) pages (2) shard (12) site (4, group root) player/ account (8) shard (8) appeals (4), behind noindex + requireAuth auth/ login (2) register (1) invite (2) password (3) session (2, root) No URL moves. All four gates zero-diff: routes.manifest.json (200 public + 2 internal), routes.guards.json, swagger-output.json (198 operations), and docs/website/api-route-inventory.json was already in sync. 434 tests green. Notes on the non-mechanical parts: - public/index.js and auth/index.js carry no group gate, deliberately, and say so. The public surface is anonymous by contract (logged-out SPA, Discord bot, Android ShardStreamClient on /public/shard/stream); /auth is where a caller becomes authenticated. player/index.js gates on requireAuth only, never requireRole('player') — staff are a superset of players. - GET /auth/me has a mount-order dependency: use('/me', meRouter) matches the bare /me, so the request runs meRouter's noindex + requireAuth and falls through. session.router.js must stay mounted last. Verified by the counterfactual — mounting it first still 401s but drops X-Robots-Tag, which no manifest or guards file can see. - loginGuards moved to auth/loginGuards.js (frozen) rather than being copied into the three routers that spread it; sso.routes.js drops its duplicate. - The :param shadowing check was re-run in dispatch order against the built stack: 86 routes, 64 literal, none shadowed. /public/wiki/{categories,tags} ahead of /:slug is the only ordering-sensitive pair. Co-Authored-By: Claude <noreply@anthropic.com>