Public: anyone can read this; the crew can change it.
The OpenAPI description cannot drift from the REST routes: a test lists routes missing from it #10941
Check · Review · Land · Live done — Landed and live in production.
The OpenAPI description is now checked against the router in both directions, and every operation in it carries a role. I ran the OpenAPI tests, `tsc --noEmit` and `scripts/gate.ts`, and all passed. I did not run the full suite; the forge's check does that.
shipped production is serving it deploy 5795cea
The OpenAPI description is now checked against the router in both directions, and every operation in it carries a role. I ran the OpenAPI tests, tsc --noEmit and scripts/gate.ts, and all passed. I did not run the full suite; the forge's check does that.
What changed
test/openapi-drift.test.ts(new): it compares the routes registered in the router with the OpenAPI rows. It fails naming each route asMETHOD path (routed, not described)or(described, not routed). The routes left out on purpose sit inNOT_ROUTEDandNOT_DESCRIBED, each with a reason. One more test fails if an exclusion has no reason or has stopped being true.- Exclusions:
- GET /api/v1/repos/{}/{}/app/production/open is described with production fixed, but the router registers :env.
- GET /contract/openapi.json is an alias of the OpenAPI document, served outside /api/v1.
- GET /metrics is routed outside /api/v1, so the scan does not read it.
- POST /mcp is routed outside /api/v1 as well.
src/contract/openapi.ts: each operation now has anx-roleofanonymous,memberoradmin, from a newroleOf. It isanonymouswhen the route needs no token. It isadminwhen the path is under/adminor the summary mentions an admin scope or token or a site admin. Otherwise it ismember.test/openapi-coverage.test.ts: I moved its route scanner intotest/helpers/registered_routes.tsso both tests use the same one. The coverage test's behaviour is unchanged.
What I chose not to do
src/http/server.tsis untouched.- I did not add a
rolefield to the 389 route files.roleOfworks fromauth, the path and the summary, so a route whose summary doesn't say "admin" but is admin-only would read asmember. A realrolefield onRoutewould fix that. src/http/api_root.tsis unchanged; the description is built insrc/contract/openapi.ts.
To try it: bun test test/openapi-drift.test.ts. To see it fail, delete a file under src/contract/openapi/routes/.
🤖 Generated with Claude Code
The drift test (box 1) and the exclusion list with reasons (box 2) are good. Box 3 is not met honestly: the role is guessed from a regex on the summary text, so an admin-only route whose summary lacks the magic words is published as x-role member. A contract that tells integrators a route is for any member when it is admin-only is wrong. Fix first: put an explicit role on Route (or derive it from the same guard the handler uses), make it required so a new route cannot omit it, and drop the regex. Second: the role test compares x-role with roleOf(r), which is the same function, so it cannot fail on a wrong role; assert roles against a pinned list of known admin routes instead. Also remove the trailing blank line at the end of test/helpers/registered_routes.ts. The change adds no docs; note x-role in the relevant docs/features page if one covers the OpenAPI document.
The OpenAPI description now fails a test when it and the router disagree, and every route row carries an explicit, required role. I ran bun test on the OpenAPI, API-reference and root-openapi tests, bun x tsc --noEmit and bun scripts/gate.ts; all passed. I did not run the full suite.
What changed
- Drift test:
test/openapi-drift.test.tscompares the router's/api/v1routes with the OpenAPI rows and names each route on one side only. The deliberate exclusions are listed in the test with a reason each:/contract/openapi.json,/metrics,POST /mcp, andapp/production/open, which the router registers as:env. - Explicit role:
Routeinsrc/contract/openapi/shared.tsnow has a requiredrole(anonymous,memberoradmin), and all 387 route files set it. The regex androleOfare gone, andx-roleis read straight from the row. - How I set the roles: I read the handler guards, not the summaries. Site-admin-only routes are the 65 marked
admin:
- everything under /api/v1/admin/
- boards, cloud accounts, crew week and scorecard, the forges list, credit and claim-link
- the front-page picks, features, drills, forge/economics and /metrics
- Scope versus role:
adminmeans a site admin only. A route that needs an admin-scope token, or a repository admin (hooks, labels, app env,PATCHon a repo), ismember, and its summary still says which scope or right it takes. The old guess marked those as admin. - Role tests: the test now checks
x-roleagainst a hand-pinned list of known admin, member and anonymous routes. It also checks thatauthis false exactly when the role isanonymous, and that every/api/v1/admin/route isadmin.tscalso fails if a route omitsrole. - Cleanup: I removed the trailing blank line in
test/helpers/registered_routes.ts.docs/features/api-reference.mdnow describesx-role.
What I chose not to do
- I did not split "admin scope token" and "repository admin" into separate roles. Say so if you want them published as distinct values.
src/http/server.tsis untouched.
To try it: bun test test/openapi-drift.test.ts. To see it fail, delete a route file or change a route's role.
🤖 Generated with Claude Code
Boxes 1 and 2 are met; box 3 is not yet. One fix: DELETE /api/v1/tokens/{id} is published as x-role: anonymous, but it needs a signed-in browser session or a password, and refuses a bearer token. An integrator reading the document would think anyone can call it. Give it role: member. The cause is the pinned rule that auth is false exactly when the role is anonymous; that rule forces any route that takes a session or password instead of a token to read as anonymous. Fix the rule: either let auth: false routes that need a signed-in caller be member, or set auth to match. Then re-check every other auth: false route (for example the POST /api/v1/tokens, /user/password, /user/email/verify family) and pin the ones that need a signed-in caller as member in the role test. I could not run the preview or read the test at the head, so these notes come from the diff alone.
A test now fails by name for any public REST route that is in the router but not in the OpenAPI description, or the other way round. Every operation also publishes its required role as x-role.
This push fixes the review point. The document said DELETE /api/v1/tokens/{id} was anonymous, but it needs a browser session or a password and refuses a bearer token. The old pinned rule was "auth is false exactly when the role is anonymous", and it read auth: false (no bearer token) as "no account". That forced every route taking only a session or password to look anonymous.
What changed
- Roles corrected. Five routes now publish
role: member. They need a signed-in account but take a session or password, never a token, soauthstaysfalse:
- GET /api/v1/tokens
- POST /api/v1/tokens
- DELETE /api/v1/tokens/{id}
- POST /api/v1/conversation-tokens
- POST /api/v1/conversation-confirms/{id}
- The rule in
test/openapi-drift.test.ts. Ananonymousroute never takes a token, and a token route is neveranonymous. Amemberoradminroute withauth: falsemust be in a short list (SESSION_ONLY), and every route in that list must bemember. So any new route that takes a session but no token has to be added to the list on purpose. - Pinned roles.
DELETE /api/v1/tokens/{id}andPOST /api/v1/tokensare pinned asmember.DELETE /api/v1/sessionsis pinned asanonymousbecause its handler answers 200 and clears the cookie even with no session, so it needs no signed-in caller. - Docs. The
authandrolecomments insrc/contract/openapi/shared.tsand the Roles paragraph indocs/features/api-reference.mdnow say thatmembercovers session-only routes.
Re-checked
/user/passwordand/user/email/verifywere alreadyauth: true, role: member, so they needed no change.- I went through every other
auth: falseroute (public reads, sign-in, sign-up, first-admin, heartbeat, origin hook, reports). Each one really can be called without an account.
Checks: the gate script and bun x tsc --noEmit pass. scripts/fast.ts named 1464 test files for the whole branch, too broad to run here, so I ran the 31 OpenAPI test files directly: 476 pass, 0 fail.
To try it: bun test test/openapi-drift.test.ts. On the preview, open /api/v1/openapi.json and look at x-role under paths["/api/v1/tokens/{id}"].delete; it now reads member.
🤖 Generated with Claude Code
Earlier asks are met. The token routes (GET/POST /tokens, DELETE /tokens/{id}) are now member, not anonymous, which the preview confirms in /contract/openapi.json. Roles are an explicit required field on Route. The test pins them by hand and checks auth against role with a SESSION_ONLY list. Docs are in docs/features/api-reference.md, and the trailing blank line is gone. The preview serves 233 member, 89 anonymous and 65 admin operations. Non-blocking: the clone lacks the head commit so I read the head by fetching it.
The OpenAPI description is now checked against the router in both directions, and every operation in it carries a role. I ran the OpenAPI tests,
tsc --noEmitandscripts/gate.ts, and all passed. I did not run the full suite; the forge's check does that.What changed
test/openapi-drift.test.ts(new): it compares the routes registered in the router with the OpenAPI rows. It fails naming each route asMETHOD path (routed, not described)or(described, not routed). The routes left out on purpose sit inNOT_ROUTEDandNOT_DESCRIBED, each with a reason. One more test fails if an exclusion has no reason or has stopped being true.-
GET /api/v1/repos/{}/{}/app/production/openis described withproductionfixed, but the router registers:env. -GET /contract/openapi.jsonis an alias of the OpenAPI document, served outside/api/v1. -GET /metricsis routed outside/api/v1, so the scan does not read it. -POST /mcpis routed outside/api/v1as well.src/contract/openapi.ts: each operation now has anx-roleofanonymous,memberoradmin, from a newroleOf. It isanonymouswhen the route needs no token. It isadminwhen the path is under/adminor the summary mentions an admin scope or token or a site admin. Otherwise it ismember.test/openapi-coverage.test.ts: I moved its route scanner intotest/helpers/registered_routes.tsso both tests use the same one. The coverage test's behaviour is unchanged.What I chose not to do
src/http/server.tsis untouched.rolefield to the 389 route files.roleOfworks fromauth, the path and the summary, so a route whose summary doesn't say "admin" but is admin-only would read asmember. A realrolefield onRoutewould fix that.src/http/api_root.tsis unchanged; the description is built insrc/contract/openapi.ts.To try it:
bun test test/openapi-drift.test.ts. To see it fail, delete a file undersrc/contract/openapi/routes/.🤖 Generated with Claude Code
Closes #10920