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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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 as METHOD path (routed, not described) or (described, not routed). The routes left out on purpose sit in NOT_ROUTED and NOT_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 an x-role of anonymous, member or admin, from a new roleOf. It is anonymous when the route needs no token. It is admin when the path is under /admin or the summary mentions an admin scope or token or a site admin. Otherwise it is member.
  • test/openapi-coverage.test.ts: I moved its route scanner into test/helpers/registered_routes.ts so both tests use the same one. The coverage test's behaviour is unchanged.

What I chose not to do

  • src/http/server.ts is untouched.
  • I did not add a role field to the 389 route files. roleOf works from auth, the path and the summary, so a route whose summary doesn't say "admin" but is admin-only would read as member. A real role field on Route would fix that.
  • src/http/api_root.ts is unchanged; the description is built in src/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

Closes #10920

commented

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 as METHOD path (routed, not described) or (described, not routed). The routes left out on purpose sit in NOT_ROUTED and NOT_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 an x-role of anonymous, member or admin, from a new roleOf. It is anonymous when the route needs no token. It is admin when the path is under /admin or the summary mentions an admin scope or token or a site admin. Otherwise it is member.
  • test/openapi-coverage.test.ts: I moved its route scanner into test/helpers/registered_routes.ts so both tests use the same one. The coverage test's behaviour is unchanged.

What I chose not to do

  • src/http/server.ts is untouched.
  • I did not add a role field to the 389 route files. roleOf works from auth, the path and the summary, so a route whose summary doesn't say "admin" but is admin-only would read as member. A real role field on Route would fix that.
  • src/http/api_root.ts is unchanged; the description is built in src/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

Changes requested — before the latest push

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.

commented

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.ts compares the router's /api/v1 routes 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, and app/production/open, which the router registers as :env.
  • Explicit role: Route in src/contract/openapi/shared.ts now has a required role (anonymous, member or admin), and all 387 route files set it. The regex and roleOf are gone, and x-role is 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: admin means a site admin only. A route that needs an admin-scope token, or a repository admin (hooks, labels, app env, PATCH on a repo), is member, 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-role against a hand-pinned list of known admin, member and anonymous routes. It also checks that auth is false exactly when the role is anonymous, and that every /api/v1/admin/ route is admin. tsc also fails if a route omits role.
  • Cleanup: I removed the trailing blank line in test/helpers/registered_routes.ts. docs/features/api-reference.md now describes x-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.ts is 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

Changes requested — before the latest push

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.

commented

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, so auth stays false:

- 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. An anonymous route never takes a token, and a token route is never anonymous. A member or admin route with auth: false must be in a short list (SESSION_ONLY), and every route in that list must be member. 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} and POST /api/v1/tokens are pinned as member. DELETE /api/v1/sessions is pinned as anonymous because its handler answers 200 and clears the cookie even with no session, so it needs no signed-in caller.
  • Docs. The auth and role comments in src/contract/openapi/shared.ts and the Roles paragraph in docs/features/api-reference.md now say that member covers session-only routes.

Re-checked

  • /user/password and /user/email/verify were already auth: true, role: member, so they needed no change.
  • I went through every other auth: false route (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

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs