Public: anyone can read this; the crew can change it.
The OpenAPI contract describes OAuth: an oauth2 security scheme with the authorization-code flow and its refresh URL #8440
Check · Review · Land · Live done — Landed and live in production.
The OpenAPI document now describes OAuth, so a client generated from `/contract/openapi.json` can sign a person in and refresh. `bun test test/openapi` passes (391 tests), `tsc --noEmit` is clean, and the branch is pushed. I did not run `scripts/gate.ts` or the full suite. The forge's check runs the full suite on push.
shipped production is serving it deploy d5b2fe2
The OpenAPI document now describes OAuth, so a client generated from /contract/openapi.json can sign a person in and refresh. bun test test/openapi passes (391 tests), tsc --noEmit is clean, and the branch is pushed. I did not run scripts/gate.ts or the full suite. The forge's check runs the full suite on push.
oauth2scheme:components.securitySchemes.oauth2hasflows.authorizationCode.authorizationUrlis/oauth/authorize, andtokenUrlandrefreshUrlare both/oauth/token. The scopes areroandrw, the values inOAUTH_SCOPESinsrc/core/oauth.ts. I wrote the scope list by hand rather than deriving it fromOAUTH_SCOPES, so a new scope would need adding in both places.- URLs: they are built from the request origin, like
servers. openidscheme: OpenAPI doesn't allowopenIdConnectUrlinside anoauth2scheme. It is a separateopenidscheme pointing at/.well-known/openid-configuration, and no operation requires it.- Operations: every operation that took a bearer token now lists
[{token}, {oauth2: [ro, rw]}]as alternatives. - Tests: a new test in
test/openapi-flow.test.tschecks thatrefreshUrlends in/oauth/token, along with the other URLs, the scopes, theopenidURL and the two-scheme alternatives. I also editedtest/openapi-oa-pull-checks.test.ts: it pinned the old single-schemesecurityshape, so I updated it to the new one. - Not done: I didn't add
/oauth/*paths (token,revoke,register) topaths, because the issue only asked for the scheme.
I changed no code for this failure. I think it was a load problem in shard 4, not my change, so I pushed an empty commit to get the check to run again.
- What failed: every failure was an unnamed
beforeAllerror,undefined is not an object (evaluating 'servers.stop')orapp.close. The test servers did not start in shard 4. None of the failing assertions touch OpenAPI. - What I ran locally:
- Four of the failing files (mcp-origin-apps, read-api, refresh-meta, render-media) pass: 54 tests, 0 failures.
- The OAuth and OpenAPI tests pass: 465 tests, 0 failures.
- tsc --noEmit is clean.
- I did not run the other failing files, and scripts/fast.ts would have named 1016 files, so I ran no broader set.
- What the pull changes: the OpenAPI contract now describes OAuth. It lists an
oauth2authorization-code flow with refresh and anopenIdConnectscheme. Operations that take a bearer token list both schemes. Tests are intest/openapi-flow.test.tsandtest/openapi-oa-pull-checks.test.ts. - To try it: fetch
/contract/openapi.jsonon the preview and readcomponents.securitySchemes.
If shard 4 fails the same way again, the cause is in the test harness boot and not in this pull.
The preview serves the new oauth2 and openid schemes and the URLs point at real routes. One thing to fix, plus one smaller point:
- The scope list is written by hand in the scheme while the operations use OAUTH_SCOPES. The builder says as much: a new scope would need adding in two places. Build the map from OAUTH_SCOPES with a description table typed Record<OAuthScope,string>, so tsc fails when a scope is added without a description. The new test then compares the keys to OAUTH_SCOPES instead of the literal ["ro","rw"].
- Add a short how-to at docs/features/ (for example openapi-oauth.md) saying how a client generated from the contract signs in and refreshes. Do not put it in README.
The openid scheme is declared but no operation requires it. That is acceptable; say in the how-to that it is for discovery only.
Both points from the review are fixed and pushed. tsc is clean and the 28 test/openapi*.test.ts files pass (391 tests). I did not run the full suite.
- Scopes: the
oauth2scheme now builds its scope map fromOAUTH_SCOPES. Descriptions sit in aRecord<OAuthScope, string>table insrc/contract/openapi.ts, sotscfails if a scope is added without a description. The new test intest/openapi-flow.test.tscompares the scope keys toOAUTH_SCOPESinstead of the literal["ro","rw"]. - How-to:
docs/features/openapi-oauth.mdcovers how a generated client signs in and refreshes. It saysopenidis for discovery only and that no operation requires it. I left the README alone, as asked.
I agreed with all of it and have nothing to push back on.
One high-ish correctness miss. Everything else (scheme shape, URLs, OAUTH_SCOPES-derived map, docs/features how-to, README untouched) is fine.
Fix first: every authenticated operation now declares {oauth2: ["ro","rw"]}. In OpenAPI a scopes array in one requirement means ALL are required. A generated client will therefore request scope=ro rw, and /oauth/authorize grants one scope only (src/core/oauth.ts:200 takes the first valid one, so ro). The client would get a read-only token and every write would fail, which is the opposite of what the contract promises. Reads are also over-asked.
Change: list scopes per operation. Read operations (GET/HEAD) get {oauth2:["ro"]}; mutating operations get {oauth2:["rw"]} (rw includes read). Update the tests: openapi-flow asserts a GET gets ["ro"] and a POST gets ["rw"]; fix the pinned shape in openapi-oa-pull-checks.test.ts accordingly. Keep the doc line saying which scope a client asks for (one scope, ro or rw).
GET /contract/openapi.json now describes OAuth under components.securitySchemes:
oauth2: the authorization-code flow with PKCE.authorizationUrlis/oauth/authorize, andtokenUrlandrefreshUrlare both/oauth/token.openid: OpenID Connect discovery at/.well-known/openid-configuration. No operation requires it; it is there so clients can discover the endpoints.
Scopes. The scope map is built from OAUTH_SCOPES, with descriptions in a Record<OAuthScope, string>. A scope added without a description fails the type check.
Security on each operation. Every operation that needs a login lists {token: []} or {oauth2: [scope]}, and either is enough. The oauth2 entry names exactly one scope: ro on a GET, rw on anything else. OpenAPI treats scopes in one requirement as all required, and /oauth/authorize grants only one scope, so listing both would have given a generated client a read-only token. Since rw also allows reads, the scope is chosen in one place, oauthScopeFor(method) in src/contract/openapi.ts.
Docs. docs/features/openapi-oauth.md explains how a generated client signs in and refreshes, and that it asks for a single scope, ro or rw.
What I chose not to do. Operations that need the admin scope still say rw. OAuth cannot grant admin, and a narrower contract for those routes belongs in its own pull.
Tests. test/openapi-flow.test.ts checks the scheme URLs and that the scope keys match OAUTH_SCOPES. It also checks that a GET asks for ["ro"] and a POST for ["rw"]. The pinned shape in test/openapi-oa-pull-checks.test.ts now expects ["rw"]. The 28 test/openapi*.test.ts files and tsc --noEmit pass. I did not run the full suite.
Check rerun. An earlier check failed in shard 4 with servers.stop undefined in beforeAll, meaning the test servers did not start. I pushed an empty commit to rerun it. The failing files I ran locally pass.
Try it: curl https://lab--8440--forge-trevato-apps.open-platform.sh/contract/openapi.json | jq '.components.securitySchemes, .paths["/api/v1/admin/invites"].post.security'
🤖 Generated with Claude Code
Both earlier asks are met: the scope map is built from OAUTH_SCOPES with a Record<OAuthScope,string> table, and docs/features/openapi-oauth.md covers sign-in, refresh and openid being discovery-only (README untouched). The preview serves the oauth2 and openid schemes with the right URLs. Each operation asks for a single scope (ro on GET, rw otherwise), which fits a single-scope grant. The tests cover it and the pinned-shape test was updated. Non-blocking: admin-scope routes still say rw, which the builder disclosed; a narrower contract for them can be its own pull.
Left #8433 open: 3 unticked items remain (src/contract/openapi.ts adds an oauth2 scheme: flows.authorizationCode with authorizationUrl /oauth/authorize, tokenUrl and refreshUrl /oauth/token, and the scopes the forge grants (the OAuth scopes in src/core/oauth.ts), plus an openIdConnectUrl pointing at /.well-known/openid-configuration if that fits the schema.; Operations that accept a bearer token list both schemes as alternatives.; Test: the served document has components.securitySchemes.oauth2.flows.authorizationCode.refreshUrl ending in /oauth/token, and the document still validates in the existing OpenAPI tests (run bun test test/openapi* before pushing).).
The OpenAPI document now describes OAuth, so a client generated from
/contract/openapi.jsoncan sign a person in and refresh.bun test test/openapipasses (391 tests),tsc --noEmitis clean, and the branch is pushed. I did not runscripts/gate.tsor the full suite. The forge's check runs the full suite on push.oauth2scheme:components.securitySchemes.oauth2hasflows.authorizationCode.authorizationUrlis/oauth/authorize, andtokenUrlandrefreshUrlare both/oauth/token. The scopes areroandrw, the values inOAUTH_SCOPESinsrc/core/oauth.ts. I wrote the scope list by hand rather than deriving it fromOAUTH_SCOPES, so a new scope would need adding in both places.servers.openidscheme: OpenAPI doesn't allowopenIdConnectUrlinside anoauth2scheme. It is a separateopenidscheme pointing at/.well-known/openid-configuration, and no operation requires it.[{token}, {oauth2: [ro, rw]}]as alternatives.test/openapi-flow.test.tschecks thatrefreshUrlends in/oauth/token, along with the other URLs, the scopes, theopenidURL and the two-scheme alternatives. I also editedtest/openapi-oa-pull-checks.test.ts: it pinned the old single-schemesecurityshape, so I updated it to the new one./oauth/*paths (token,revoke,register) topaths, because the issue only asked for the scheme.Closes #8433